firefox-devtools-mcp 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12192,6 +12192,9 @@ var init_core = __esm({
12192
12192
  if (this.options.profilePath) {
12193
12193
  firefoxOptions.setProfile(this.options.profilePath);
12194
12194
  }
12195
+ if (this.options.acceptInsecureCerts) {
12196
+ firefoxOptions.setAcceptInsecureCerts(true);
12197
+ }
12195
12198
  this.driver = await new Builder().forBrowser(Browser.FIREFOX).setFirefoxOptions(firefoxOptions).build();
12196
12199
  log("\u2705 Firefox launched with BiDi");
12197
12200
  this.currentContextId = await this.driver.getWindowHandle();
@@ -12211,6 +12214,30 @@ var init_core = __esm({
12211
12214
  }
12212
12215
  return this.driver;
12213
12216
  }
12217
+ /**
12218
+ * Check if Firefox is still connected and responsive
12219
+ * Returns false if Firefox was closed or connection is broken
12220
+ */
12221
+ async isConnected() {
12222
+ if (!this.driver) {
12223
+ return false;
12224
+ }
12225
+ try {
12226
+ await this.driver.getWindowHandle();
12227
+ return true;
12228
+ } catch (error) {
12229
+ logDebug("Connection check failed: Firefox is not responsive");
12230
+ return false;
12231
+ }
12232
+ }
12233
+ /**
12234
+ * Reset driver state (used when Firefox is detected as closed)
12235
+ */
12236
+ reset() {
12237
+ this.driver = null;
12238
+ this.currentContextId = null;
12239
+ logDebug("Driver state reset");
12240
+ }
12214
12241
  /**
12215
12242
  * Get current browsing context ID
12216
12243
  */
@@ -12555,10 +12582,42 @@ var init_network = __esm({
12555
12582
  */
12556
12583
  parseHeaders(headers) {
12557
12584
  const result = {};
12585
+ const normalizeValue = (value) => {
12586
+ if (value === null || value === void 0) {
12587
+ return null;
12588
+ }
12589
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
12590
+ return String(value);
12591
+ }
12592
+ if (Array.isArray(value)) {
12593
+ const parts = value.map((item) => normalizeValue(item)).filter((item) => !!item);
12594
+ return parts.length > 0 ? parts.join(", ") : null;
12595
+ }
12596
+ if (typeof value === "object") {
12597
+ const obj = value;
12598
+ if ("value" in obj) {
12599
+ return normalizeValue(obj.value);
12600
+ }
12601
+ if ("bytes" in obj) {
12602
+ return normalizeValue(obj.bytes);
12603
+ }
12604
+ try {
12605
+ return JSON.stringify(obj);
12606
+ } catch {
12607
+ return null;
12608
+ }
12609
+ }
12610
+ return String(value);
12611
+ };
12558
12612
  if (Array.isArray(headers)) {
12559
12613
  for (const h of headers) {
12560
- if (h.name && h.value) {
12561
- result[h.name.toLowerCase()] = String(h.value);
12614
+ const name = h?.name ? String(h.name).toLowerCase() : "";
12615
+ if (!name) {
12616
+ continue;
12617
+ }
12618
+ const normalizedValue = normalizeValue(h?.value);
12619
+ if (normalizedValue !== null) {
12620
+ result[name] = normalizedValue;
12562
12621
  }
12563
12622
  }
12564
12623
  }
@@ -12986,6 +13045,7 @@ var init_pages = __esm({
12986
13045
  if (index >= 0 && index < handles.length) {
12987
13046
  await this.driver.switchTo().window(handles[index]);
12988
13047
  this.setCurrentContextId(handles[index]);
13048
+ this.cachedSelectedIdx = index;
12989
13049
  }
12990
13050
  }
12991
13051
  /**
@@ -12996,6 +13056,7 @@ var init_pages = __esm({
12996
13056
  const handles = await this.driver.getAllWindowHandles();
12997
13057
  const newIdx = handles.length - 1;
12998
13058
  this.setCurrentContextId(handles[newIdx]);
13059
+ this.cachedSelectedIdx = newIdx;
12999
13060
  await this.driver.get(url);
13000
13061
  return newIdx;
13001
13062
  }
@@ -13131,7 +13192,7 @@ var MAX_ATTR_LENGTH;
13131
13192
  var init_formatter = __esm({
13132
13193
  "src/firefox/snapshot/formatter.ts"() {
13133
13194
  "use strict";
13134
- MAX_ATTR_LENGTH = 50;
13195
+ MAX_ATTR_LENGTH = 30;
13135
13196
  }
13136
13197
  });
13137
13198
 
@@ -13300,10 +13361,12 @@ var init_manager = __esm({
13300
13361
  const currentFilePath = fileURLToPath(currentFileUrl);
13301
13362
  const currentDir = dirname(currentFilePath);
13302
13363
  const possiblePaths = [
13303
- // Production: relative to compiled dist/index.js location
13304
- resolve(currentDir, "../../snapshot.injected.global.js"),
13305
- // Alternative: relative to current working directory
13306
- resolve(process.cwd(), "dist/snapshot.injected.global.js")
13364
+ // Production: relative to bundled dist/index.js (same directory)
13365
+ resolve(currentDir, "snapshot.injected.global.js"),
13366
+ // Development: relative to current working directory
13367
+ resolve(process.cwd(), "dist/snapshot.injected.global.js"),
13368
+ // npx: package is in node_modules, try to find it relative to the binary
13369
+ resolve(currentDir, "../snapshot.injected.global.js")
13307
13370
  ];
13308
13371
  const attemptedPaths = [];
13309
13372
  for (const path of possiblePaths) {
@@ -13358,7 +13421,8 @@ ${attemptedPaths.map((p) => ` - ${p}`).join("\n")}`
13358
13421
  root: result.tree,
13359
13422
  snapshotId,
13360
13423
  timestamp: Date.now(),
13361
- truncated: result.truncated || false
13424
+ truncated: result.truncated || false,
13425
+ uidMap: result.uidMap
13362
13426
  };
13363
13427
  const snapshot = {
13364
13428
  text: formatSnapshotTree(result.tree),
@@ -13726,6 +13790,24 @@ var init_firefox = __esm({
13726
13790
  getDriver() {
13727
13791
  return this.core.getDriver();
13728
13792
  }
13793
+ /**
13794
+ * Check if Firefox is still connected and responsive
13795
+ * Returns false if Firefox was closed or connection is broken
13796
+ */
13797
+ async isConnected() {
13798
+ return await this.core.isConnected();
13799
+ }
13800
+ /**
13801
+ * Reset all internal state (used when Firefox is detected as closed)
13802
+ */
13803
+ reset() {
13804
+ this.core.reset();
13805
+ this.consoleEvents = null;
13806
+ this.networkEvents = null;
13807
+ this.dom = null;
13808
+ this.pages = null;
13809
+ this.snapshot = null;
13810
+ }
13729
13811
  // ============================================================================
13730
13812
  // Cleanup
13731
13813
  // ============================================================================
@@ -13737,6 +13819,30 @@ var init_firefox = __esm({
13737
13819
  });
13738
13820
 
13739
13821
  // src/utils/response-helpers.ts
13822
+ function truncateText(text, maxChars, suffix = "\n\n[... truncated - exceeded size limit]") {
13823
+ if (text.length <= maxChars) {
13824
+ return text;
13825
+ }
13826
+ return text.slice(0, maxChars - suffix.length) + suffix;
13827
+ }
13828
+ function truncateHeaders(headers) {
13829
+ if (!headers) {
13830
+ return null;
13831
+ }
13832
+ const result = {};
13833
+ let totalChars = 0;
13834
+ for (const [key, value] of Object.entries(headers)) {
13835
+ const truncatedValue = value.length > TOKEN_LIMITS.MAX_HEADER_VALUE_CHARS ? value.slice(0, TOKEN_LIMITS.MAX_HEADER_VALUE_CHARS) + "...[truncated]" : value;
13836
+ const entrySize = key.length + truncatedValue.length;
13837
+ if (totalChars + entrySize > TOKEN_LIMITS.MAX_HEADERS_TOTAL_CHARS) {
13838
+ result["__truncated__"] = "Headers truncated due to size limit";
13839
+ break;
13840
+ }
13841
+ result[key] = truncatedValue;
13842
+ totalChars += entrySize;
13843
+ }
13844
+ return result;
13845
+ }
13740
13846
  function successResponse(message) {
13741
13847
  return {
13742
13848
  content: [
@@ -13769,26 +13875,40 @@ function jsonResponse(data) {
13769
13875
  ]
13770
13876
  };
13771
13877
  }
13878
+ var TOKEN_LIMITS;
13772
13879
  var init_response_helpers = __esm({
13773
13880
  "src/utils/response-helpers.ts"() {
13774
13881
  "use strict";
13882
+ TOKEN_LIMITS = {
13883
+ /** Maximum characters for a single response (~12.5k tokens at ~4 chars/token) */
13884
+ MAX_RESPONSE_CHARS: 5e4,
13885
+ /** Maximum characters for screenshot base64 data (~10k tokens) */
13886
+ MAX_SCREENSHOT_CHARS: 4e4,
13887
+ /** Maximum characters per console message text */
13888
+ MAX_CONSOLE_MESSAGE_CHARS: 2e3,
13889
+ /** Maximum characters for network header values (per header) */
13890
+ MAX_HEADER_VALUE_CHARS: 500,
13891
+ /** Maximum total characters for all headers combined */
13892
+ MAX_HEADERS_TOTAL_CHARS: 5e3,
13893
+ /** Hard cap on snapshot lines (even if user requests more) */
13894
+ MAX_SNAPSHOT_LINES_CAP: 500,
13895
+ /** Warning threshold - show warning when response exceeds this */
13896
+ WARNING_THRESHOLD_CHARS: 3e4
13897
+ };
13775
13898
  }
13776
13899
  });
13777
13900
 
13778
13901
  // src/tools/pages.ts
13779
13902
  function formatPageList(tabs, selectedIdx) {
13780
- const lines = [`\u{1F4C4} Open pages (${tabs.length} total, selected: [${selectedIdx}]):`];
13781
13903
  if (tabs.length === 0) {
13782
- lines.push(" (no pages open)");
13783
- } else {
13784
- for (const tab of tabs) {
13785
- const idx = tabs.indexOf(tab);
13786
- const indicator = idx === selectedIdx ? "\u{1F449}" : " ";
13787
- const title = tab.title || "Untitled";
13788
- const url = tab.url || "about:blank";
13789
- lines.push(`${indicator} [${idx}] ${title}`);
13790
- lines.push(` ${url}`);
13791
- }
13904
+ return "\u{1F4C4} No pages";
13905
+ }
13906
+ const lines = [`\u{1F4C4} ${tabs.length} pages (selected: ${selectedIdx})`];
13907
+ for (const tab of tabs) {
13908
+ const idx = tabs.indexOf(tab);
13909
+ const marker = idx === selectedIdx ? ">" : " ";
13910
+ const title = (tab.title || "Untitled").substring(0, 40);
13911
+ lines.push(`${marker}[${idx}] ${title}`);
13792
13912
  }
13793
13913
  return lines.join("\n");
13794
13914
  }
@@ -13813,14 +13933,7 @@ async function handleNewPage(args2) {
13813
13933
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
13814
13934
  const firefox3 = await getFirefox2();
13815
13935
  const newIdx = await firefox3.createNewPage(url);
13816
- await firefox3.refreshTabs();
13817
- const tabs = firefox3.getTabs();
13818
- const newTab = tabs[newIdx];
13819
- return successResponse(
13820
- `\u2705 Created new page [${newIdx}] and navigated to: ${url}
13821
- Title: ${newTab?.title || "Loading..."}
13822
- Total pages: ${tabs.length}`
13823
- );
13936
+ return successResponse(`\u2705 new page [${newIdx}] \u2192 ${url}`);
13824
13937
  } catch (error) {
13825
13938
  return errorResponse(error);
13826
13939
  }
@@ -13841,10 +13954,7 @@ async function handleNavigatePage(args2) {
13841
13954
  throw new Error("No page selected");
13842
13955
  }
13843
13956
  await firefox3.navigate(url);
13844
- return successResponse(
13845
- `\u2705 Navigated page [${selectedIdx}] to: ${url}
13846
- Previous URL: ${page.url}`
13847
- );
13957
+ return successResponse(`\u2705 [${selectedIdx}] \u2192 ${url}`);
13848
13958
  } catch (error) {
13849
13959
  return errorResponse(error);
13850
13960
  }
@@ -13857,47 +13967,30 @@ async function handleSelectPage(args2) {
13857
13967
  await firefox3.refreshTabs();
13858
13968
  const tabs = firefox3.getTabs();
13859
13969
  let selectedIdx;
13860
- let selectionMethod;
13861
13970
  if (typeof pageIdx === "number") {
13862
13971
  selectedIdx = pageIdx;
13863
- selectionMethod = "by index";
13864
13972
  } else if (url && typeof url === "string") {
13865
13973
  const urlLower = url.toLowerCase();
13866
13974
  const foundIdx = tabs.findIndex((tab) => tab.url?.toLowerCase().includes(urlLower));
13867
13975
  if (foundIdx === -1) {
13868
- throw new Error(
13869
- `No page found with URL matching "${url}". Use list_pages to see all available pages.`
13870
- );
13976
+ throw new Error(`No page matching URL "${url}"`);
13871
13977
  }
13872
13978
  selectedIdx = foundIdx;
13873
- selectionMethod = `by URL pattern "${url}"`;
13874
13979
  } else if (title && typeof title === "string") {
13875
13980
  const titleLower = title.toLowerCase();
13876
13981
  const foundIdx = tabs.findIndex((tab) => tab.title?.toLowerCase().includes(titleLower));
13877
13982
  if (foundIdx === -1) {
13878
- throw new Error(
13879
- `No page found with title matching "${title}". Use list_pages to see all available pages.`
13880
- );
13983
+ throw new Error(`No page matching title "${title}"`);
13881
13984
  }
13882
13985
  selectedIdx = foundIdx;
13883
- selectionMethod = `by title pattern "${title}"`;
13884
13986
  } else {
13885
- throw new Error(
13886
- "At least one of pageIdx, url, or title must be provided. Use list_pages to see available pages."
13887
- );
13987
+ throw new Error("Provide pageIdx, url, or title");
13888
13988
  }
13889
- const page = tabs[selectedIdx];
13890
- if (!page) {
13891
- throw new Error(
13892
- `Page at index ${selectedIdx} not found. Use list_pages to see valid indices.`
13893
- );
13989
+ if (!tabs[selectedIdx]) {
13990
+ throw new Error(`Page [${selectedIdx}] not found`);
13894
13991
  }
13895
13992
  await firefox3.selectTab(selectedIdx);
13896
- return successResponse(
13897
- `\u2705 Selected page [${selectedIdx}] ${selectionMethod}
13898
- Title: ${page.title || "Untitled"}
13899
- URL: ${page.url || "about:blank"}`
13900
- );
13993
+ return successResponse(`\u2705 selected [${selectedIdx}]`);
13901
13994
  } catch (error) {
13902
13995
  return errorResponse(error);
13903
13996
  }
@@ -13917,10 +14010,7 @@ async function handleClosePage(args2) {
13917
14010
  throw new Error(`Page with index ${pageIdx} not found`);
13918
14011
  }
13919
14012
  await firefox3.closeTab(pageIdx);
13920
- return successResponse(
13921
- `\u2705 Closed page [${pageIdx}]: ${pageToClose.title}
13922
- ${pageToClose.url}`
13923
- );
14013
+ return successResponse(`\u2705 closed [${pageIdx}]`);
13924
14014
  } catch (error) {
13925
14015
  return errorResponse(error);
13926
14016
  }
@@ -13932,7 +14022,7 @@ var init_pages2 = __esm({
13932
14022
  init_response_helpers();
13933
14023
  listPagesTool = {
13934
14024
  name: "list_pages",
13935
- description: "List all open tabs with index, title, and URL. The currently selected tab is marked. Use the index with select_page.",
14025
+ description: "List open tabs (index, title, URL). Selected tab is marked.",
13936
14026
  inputSchema: {
13937
14027
  type: "object",
13938
14028
  properties: {}
@@ -13940,13 +14030,13 @@ var init_pages2 = __esm({
13940
14030
  };
13941
14031
  newPageTool = {
13942
14032
  name: "new_page",
13943
- description: "Open a new tab and navigate it to the provided URL. Returns the new tab index in the response.",
14033
+ description: "Open new tab at URL. Returns tab index.",
13944
14034
  inputSchema: {
13945
14035
  type: "object",
13946
14036
  properties: {
13947
14037
  url: {
13948
14038
  type: "string",
13949
- description: "URL to load in a new page"
14039
+ description: "Target URL"
13950
14040
  }
13951
14041
  },
13952
14042
  required: ["url"]
@@ -13954,13 +14044,13 @@ var init_pages2 = __esm({
13954
14044
  };
13955
14045
  navigatePageTool = {
13956
14046
  name: "navigate_page",
13957
- description: "Navigate the currently selected tab to the provided URL.",
14047
+ description: "Navigate selected tab to URL.",
13958
14048
  inputSchema: {
13959
14049
  type: "object",
13960
14050
  properties: {
13961
14051
  url: {
13962
14052
  type: "string",
13963
- description: "URL to navigate the page to"
14053
+ description: "Target URL"
13964
14054
  }
13965
14055
  },
13966
14056
  required: ["url"]
@@ -13968,21 +14058,21 @@ var init_pages2 = __esm({
13968
14058
  };
13969
14059
  selectPageTool = {
13970
14060
  name: "select_page",
13971
- description: "Select the active tab by index (preferred), or by matching URL/title. Index takes precedence when multiple parameters are provided.",
14061
+ description: "Select active tab by index, URL, or title. Index takes precedence.",
13972
14062
  inputSchema: {
13973
14063
  type: "object",
13974
14064
  properties: {
13975
14065
  pageIdx: {
13976
14066
  type: "number",
13977
- description: "The index of the page to select (e.g., 0, 1, 2). Use list_pages first to see all available page indices. Most reliable method."
14067
+ description: "Tab index (0-based, most reliable)"
13978
14068
  },
13979
14069
  url: {
13980
14070
  type: "string",
13981
- description: 'Select page by URL (partial match, case-insensitive). Example: "github.com" will match "https://github.com/user/repo"'
14071
+ description: "URL substring (case-insensitive)"
13982
14072
  },
13983
14073
  title: {
13984
14074
  type: "string",
13985
- description: 'Select page by title (partial match, case-insensitive). Example: "Google" will match "Google Search - About"'
14075
+ description: "Title substring (case-insensitive)"
13986
14076
  }
13987
14077
  },
13988
14078
  required: []
@@ -13990,13 +14080,13 @@ var init_pages2 = __esm({
13990
14080
  };
13991
14081
  closePageTool = {
13992
14082
  name: "close_page",
13993
- description: "Close the tab at the given index. Use list_pages to find valid indices.",
14083
+ description: "Close tab by index.",
13994
14084
  inputSchema: {
13995
14085
  type: "object",
13996
14086
  properties: {
13997
14087
  pageIdx: {
13998
14088
  type: "number",
13999
- description: "The index of the page to close"
14089
+ description: "Tab index to close"
14000
14090
  }
14001
14091
  },
14002
14092
  required: ["pageIdx"]
@@ -14034,6 +14124,10 @@ async function handleListConsoleMessages(args2) {
14034
14124
  if (source) {
14035
14125
  messages = messages.filter((msg) => msg.source?.toLowerCase() === source.toLowerCase());
14036
14126
  }
14127
+ messages = messages.map((msg) => ({
14128
+ ...msg,
14129
+ text: truncateText(msg.text, TOKEN_LIMITS.MAX_CONSOLE_MESSAGE_CHARS, "...[truncated]")
14130
+ }));
14037
14131
  const maxLimit = limit ?? DEFAULT_LIMIT;
14038
14132
  const filteredCount = messages.length;
14039
14133
  const truncated = messages.length > maxLimit;
@@ -14127,7 +14221,7 @@ Total messages: ${totalCount}${filterInfo.length > 0 ? `, Filters: ${filterInfo.
14127
14221
  }
14128
14222
  if (truncated) {
14129
14223
  output += `
14130
- ... ${filteredCount - messages.length} more messages (increase limit to see more)`;
14224
+ [+${filteredCount - messages.length} more]`;
14131
14225
  }
14132
14226
  return successResponse(output);
14133
14227
  } catch (error) {
@@ -14140,11 +14234,7 @@ async function handleClearConsoleMessages(_args) {
14140
14234
  const firefox3 = await getFirefox2();
14141
14235
  const count = (await firefox3.getConsoleMessages()).length;
14142
14236
  firefox3.clearConsoleMessages();
14143
- return successResponse(
14144
- `Cleared ${count} console message(s) from buffer.
14145
-
14146
- You can now capture fresh console output from new page activity.`
14147
- );
14237
+ return successResponse(`\u2705 cleared ${count} messages`);
14148
14238
  } catch (error) {
14149
14239
  return errorResponse(error);
14150
14240
  }
@@ -14156,42 +14246,42 @@ var init_console2 = __esm({
14156
14246
  init_response_helpers();
14157
14247
  listConsoleMessagesTool = {
14158
14248
  name: "list_console_messages",
14159
- description: "List console messages for the selected tab since the last navigation. Use filters (level, limit, sinceMs, textContains, source) to focus on recent and relevant logs.",
14249
+ description: "List console messages. Supports filtering by level, time, text, source.",
14160
14250
  inputSchema: {
14161
14251
  type: "object",
14162
14252
  properties: {
14163
14253
  level: {
14164
14254
  type: "string",
14165
14255
  enum: ["debug", "info", "warn", "error"],
14166
- description: "Filter by console message level"
14256
+ description: "Filter by level"
14167
14257
  },
14168
14258
  limit: {
14169
14259
  type: "number",
14170
- description: "Maximum number of messages to return (default: 50)"
14260
+ description: "Max messages (default: 50)"
14171
14261
  },
14172
14262
  sinceMs: {
14173
14263
  type: "number",
14174
- description: "Only show messages from the last N milliseconds (filters by timestamp)"
14264
+ description: "Only last N ms"
14175
14265
  },
14176
14266
  textContains: {
14177
14267
  type: "string",
14178
- description: "Filter messages by text content (case-insensitive substring match)"
14268
+ description: "Text filter (case-insensitive)"
14179
14269
  },
14180
14270
  source: {
14181
14271
  type: "string",
14182
- description: 'Filter messages by source (e.g., "console-api", "javascript", "network")'
14272
+ description: "Filter by source"
14183
14273
  },
14184
14274
  format: {
14185
14275
  type: "string",
14186
14276
  enum: ["text", "json"],
14187
- description: "Output format: text (default, human-readable) or json (structured data)"
14277
+ description: "Output format (default: text)"
14188
14278
  }
14189
14279
  }
14190
14280
  }
14191
14281
  };
14192
14282
  clearConsoleMessagesTool = {
14193
14283
  name: "clear_console_messages",
14194
- description: "Clear the collected console messages. TIP: Clear before a new measurement to keep output focused.",
14284
+ description: "Clear collected console messages.",
14195
14285
  inputSchema: {
14196
14286
  type: "object",
14197
14287
  properties: {}
@@ -14292,8 +14382,8 @@ async function handleListNetworkRequests(args2) {
14292
14382
  resourceType: req.resourceType,
14293
14383
  isXHR: req.isXHR,
14294
14384
  timings: req.timings || null,
14295
- requestHeaders: req.requestHeaders || null,
14296
- responseHeaders: req.responseHeaders || null
14385
+ requestHeaders: truncateHeaders(req.requestHeaders),
14386
+ responseHeaders: truncateHeaders(req.responseHeaders)
14297
14387
  }));
14298
14388
  }
14299
14389
  return jsonResponse(responseData);
@@ -14303,15 +14393,9 @@ async function handleListNetworkRequests(args2) {
14303
14393
  const statusInfo = req.status ? `[${req.status}${req.statusText ? " " + req.statusText : ""}]` : "[pending]";
14304
14394
  return `${req.id} | ${req.method} ${req.url} ${statusInfo}${req.isXHR ? " (XHR)" : ""}`;
14305
14395
  });
14306
- const summary = [
14307
- `Found ${requests.length} network request(s)${hasMore ? ` (showing first ${limit})` : ""}`,
14308
- "",
14309
- "Network Requests:",
14310
- ...formattedRequests,
14311
- "",
14312
- "TIP: Use the request ID (first column) with get_network_request for full details."
14313
- ].join("\n");
14314
- return successResponse(summary);
14396
+ const header = `\u{1F4E1} ${requests.length} requests${hasMore ? ` (limit ${limit})` : ""}
14397
+ `;
14398
+ return successResponse(header + formattedRequests.join("\n"));
14315
14399
  } else if (detail === "min") {
14316
14400
  const minData = limitedRequests.map((req) => ({
14317
14401
  id: req.id,
@@ -14324,8 +14408,7 @@ async function handleListNetworkRequests(args2) {
14324
14408
  duration: req.timings?.duration
14325
14409
  }));
14326
14410
  return successResponse(
14327
- `Found ${requests.length} requests${hasMore ? ` (showing first ${limit})` : ""}
14328
-
14411
+ `\u{1F4E1} ${requests.length} requests${hasMore ? ` (limit ${limit})` : ""}
14329
14412
  ` + JSON.stringify(minData, null, 2)
14330
14413
  );
14331
14414
  } else {
@@ -14338,19 +14421,16 @@ async function handleListNetworkRequests(args2) {
14338
14421
  resourceType: req.resourceType,
14339
14422
  isXHR: req.isXHR,
14340
14423
  timings: req.timings || null,
14341
- requestHeaders: req.requestHeaders || null,
14342
- responseHeaders: req.responseHeaders || null
14424
+ requestHeaders: truncateHeaders(req.requestHeaders),
14425
+ responseHeaders: truncateHeaders(req.responseHeaders)
14343
14426
  }));
14344
14427
  return successResponse(
14345
- `Found ${requests.length} requests${hasMore ? ` (showing first ${limit})` : ""}
14346
-
14428
+ `\u{1F4E1} ${requests.length} requests${hasMore ? ` (limit ${limit})` : ""}
14347
14429
  ` + JSON.stringify(fullData, null, 2)
14348
14430
  );
14349
14431
  }
14350
14432
  } catch (error) {
14351
- return errorResponse(
14352
- `Failed to list network requests: ${error instanceof Error ? error.message : String(error)}`
14353
- );
14433
+ return errorResponse(error instanceof Error ? error : new Error(String(error)));
14354
14434
  }
14355
14435
  }
14356
14436
  async function handleGetNetworkRequest(args2) {
@@ -14361,9 +14441,7 @@ async function handleGetNetworkRequest(args2) {
14361
14441
  format = "text"
14362
14442
  } = args2;
14363
14443
  if (!id && !url) {
14364
- return errorResponse(
14365
- 'Either "id" or "url" parameter is required.\n\nTIP: Call list_network_requests first and use the returned ID for reliable lookup.'
14366
- );
14444
+ return errorResponse("id or url required");
14367
14445
  }
14368
14446
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
14369
14447
  const firefox3 = await getFirefox2();
@@ -14372,29 +14450,16 @@ async function handleGetNetworkRequest(args2) {
14372
14450
  if (id) {
14373
14451
  request = requests.find((req) => req.id === id);
14374
14452
  if (!request) {
14375
- return errorResponse(
14376
- `No network request found with ID: ${id}
14377
-
14378
- TIP: The request may have been cleared. Call list_network_requests to see available requests.`
14379
- );
14453
+ return errorResponse(`ID ${id} not found`);
14380
14454
  }
14381
14455
  } else if (url) {
14382
14456
  const matches = requests.filter((req) => req.url === url);
14383
14457
  if (matches.length === 0) {
14384
- return errorResponse(
14385
- `No network request found with URL: ${url}
14386
-
14387
- TIP: Use list_network_requests to see available requests.`
14388
- );
14458
+ return errorResponse(`URL not found: ${url}`);
14389
14459
  }
14390
14460
  if (matches.length > 1) {
14391
- const matchInfo = matches.map((req) => ` - ID: ${req.id} | ${req.method} [${req.status || "pending"}]`).join("\n");
14392
- return errorResponse(
14393
- `Multiple requests (${matches.length}) found with URL: ${url}
14394
-
14395
- Please use one of these IDs with the "id" parameter:
14396
- ` + matchInfo
14397
- );
14461
+ const ids = matches.map((req) => req.id).join(", ");
14462
+ return errorResponse(`Multiple matches, use id: ${ids}`);
14398
14463
  }
14399
14464
  request = matches[0];
14400
14465
  }
@@ -14411,17 +14476,15 @@ Please use one of these IDs with the "id" parameter:
14411
14476
  isXHR: request.isXHR ?? false,
14412
14477
  timestamp: request.timestamp ?? null,
14413
14478
  timings: request.timings ?? null,
14414
- requestHeaders: request.requestHeaders ?? null,
14415
- responseHeaders: request.responseHeaders ?? null
14479
+ requestHeaders: truncateHeaders(request.requestHeaders),
14480
+ responseHeaders: truncateHeaders(request.responseHeaders)
14416
14481
  };
14417
14482
  if (format === "json") {
14418
14483
  return jsonResponse(details);
14419
14484
  }
14420
- return successResponse("Network Request Details:\n\n" + JSON.stringify(details, null, 2));
14485
+ return successResponse(JSON.stringify(details, null, 2));
14421
14486
  } catch (error) {
14422
- return errorResponse(
14423
- `Failed to get network request: ${error instanceof Error ? error.message : String(error)}`
14424
- );
14487
+ return errorResponse(error instanceof Error ? error : new Error(String(error)));
14425
14488
  }
14426
14489
  }
14427
14490
  var listNetworkRequestsTool, getNetworkRequestTool;
@@ -14431,82 +14494,82 @@ var init_network2 = __esm({
14431
14494
  init_response_helpers();
14432
14495
  listNetworkRequestsTool = {
14433
14496
  name: "list_network_requests",
14434
- description: "List recent network requests across all tabs. Network capture is always on. Use filters (limit, sinceMs, urlContains, method, status, resourceType) and detail (summary|min|full) to control output. Each entry includes a stable id for use with get_network_request.",
14497
+ description: "List network requests. Returns IDs for get_network_request.",
14435
14498
  inputSchema: {
14436
14499
  type: "object",
14437
14500
  properties: {
14438
14501
  limit: {
14439
14502
  type: "number",
14440
- description: "Maximum number of requests to return (default: 50)"
14503
+ description: "Max requests (default: 50)"
14441
14504
  },
14442
14505
  sinceMs: {
14443
14506
  type: "number",
14444
- description: "Return only requests newer than N milliseconds ago"
14507
+ description: "Only last N ms"
14445
14508
  },
14446
14509
  urlContains: {
14447
14510
  type: "string",
14448
- description: "Filter requests by URL substring (case-insensitive)"
14511
+ description: "URL filter (case-insensitive)"
14449
14512
  },
14450
14513
  method: {
14451
14514
  type: "string",
14452
- description: "Filter by HTTP method (GET, POST, etc., case-insensitive)"
14515
+ description: "HTTP method filter"
14453
14516
  },
14454
14517
  status: {
14455
14518
  type: "number",
14456
- description: "Filter by exact HTTP status code"
14519
+ description: "Exact status code"
14457
14520
  },
14458
14521
  statusMin: {
14459
14522
  type: "number",
14460
- description: "Filter by minimum HTTP status code"
14523
+ description: "Min status code"
14461
14524
  },
14462
14525
  statusMax: {
14463
14526
  type: "number",
14464
- description: "Filter by maximum HTTP status code"
14527
+ description: "Max status code"
14465
14528
  },
14466
14529
  isXHR: {
14467
14530
  type: "boolean",
14468
- description: "Filter by XHR/fetch requests only"
14531
+ description: "XHR/fetch only"
14469
14532
  },
14470
14533
  resourceType: {
14471
14534
  type: "string",
14472
- description: "Filter by resource type (case-insensitive)"
14535
+ description: "Resource type filter"
14473
14536
  },
14474
14537
  sortBy: {
14475
14538
  type: "string",
14476
14539
  enum: ["timestamp", "duration", "status"],
14477
- description: "Sort requests by field (default: timestamp descending)"
14540
+ description: "Sort field (default: timestamp)"
14478
14541
  },
14479
14542
  detail: {
14480
14543
  type: "string",
14481
14544
  enum: ["summary", "min", "full"],
14482
- description: "Output detail level: summary (default), min (compact JSON), full (includes headers)"
14545
+ description: "Detail level (default: summary)"
14483
14546
  },
14484
14547
  format: {
14485
14548
  type: "string",
14486
14549
  enum: ["text", "json"],
14487
- description: "Output format: text (default, human-readable) or json (structured data)"
14550
+ description: "Output format (default: text)"
14488
14551
  }
14489
14552
  }
14490
14553
  }
14491
14554
  };
14492
14555
  getNetworkRequestTool = {
14493
14556
  name: "get_network_request",
14494
- description: "Get detailed information about a network request by id (recommended). URL lookup is available as a fallback but may match multiple requests.",
14557
+ description: "Get request details by ID. URL lookup as fallback.",
14495
14558
  inputSchema: {
14496
14559
  type: "object",
14497
14560
  properties: {
14498
14561
  id: {
14499
14562
  type: "string",
14500
- description: "The request ID from list_network_requests (recommended)"
14563
+ description: "Request ID from list_network_requests"
14501
14564
  },
14502
14565
  url: {
14503
14566
  type: "string",
14504
- description: "The URL of the request (fallback, may match multiple requests)"
14567
+ description: "URL fallback (may match multiple)"
14505
14568
  },
14506
14569
  format: {
14507
14570
  type: "string",
14508
14571
  enum: ["text", "json"],
14509
- description: "Output format: text (default) or json (structured data)"
14572
+ description: "Output format (default: text)"
14510
14573
  }
14511
14574
  }
14512
14575
  }
@@ -14514,15 +14577,31 @@ var init_network2 = __esm({
14514
14577
  }
14515
14578
  });
14516
14579
 
14580
+ // src/utils/uid-helpers.ts
14581
+ function handleUidError(error, uid) {
14582
+ const errorMsg = error.message;
14583
+ if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID") || errorMsg.includes("not found")) {
14584
+ return new Error(`${uid} stale/invalid. Call take_snapshot first.`);
14585
+ }
14586
+ return error;
14587
+ }
14588
+ var init_uid_helpers = __esm({
14589
+ "src/utils/uid-helpers.ts"() {
14590
+ "use strict";
14591
+ }
14592
+ });
14593
+
14517
14594
  // src/tools/snapshot.ts
14518
14595
  async function handleTakeSnapshot(args2) {
14519
14596
  try {
14520
14597
  const {
14521
- maxLines = MAX_SNAPSHOT_LINES,
14598
+ maxLines: requestedMaxLines = DEFAULT_SNAPSHOT_LINES,
14522
14599
  includeAttributes = false,
14523
14600
  includeText = true,
14524
14601
  maxDepth
14525
14602
  } = args2 || {};
14603
+ const maxLines = Math.min(Math.max(1, requestedMaxLines), TOKEN_LIMITS.MAX_SNAPSHOT_LINES_CAP);
14604
+ const wasCapped = requestedMaxLines > TOKEN_LIMITS.MAX_SNAPSHOT_LINES_CAP;
14526
14605
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
14527
14606
  const firefox3 = await getFirefox2();
14528
14607
  const snapshot = await firefox3.takeSnapshot();
@@ -14538,23 +14617,19 @@ async function handleTakeSnapshot(args2) {
14538
14617
  const lines = formattedText.split("\n");
14539
14618
  const truncated = lines.length > maxLines;
14540
14619
  const displayLines = truncated ? lines.slice(0, maxLines) : lines;
14541
- let output = "\u{1F4F8} Snapshot taken\n\n";
14542
- output += "\u2550\u2550\u2550 HOW TO USE THIS SNAPSHOT \u2550\u2550\u2550\n";
14543
- output += "\u2022 To interact with elements: use click_by_uid, hover_by_uid, or fill_by_uid with the UID\n";
14544
- output += "\u2022 After navigation: always call take_snapshot again (UIDs become stale)\n";
14545
- output += "\u2022 On stale UID errors: call take_snapshot \u2192 retry your action\n";
14546
- output += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n";
14547
- output += `Snapshot ID: ${snapshot.json.snapshotId}
14548
- `;
14620
+ let output = `\u{1F4F8} Snapshot (id=${snapshot.json.snapshotId})`;
14621
+ if (wasCapped) {
14622
+ output += ` [maxLines capped: ${TOKEN_LIMITS.MAX_SNAPSHOT_LINES_CAP}]`;
14623
+ }
14549
14624
  if (snapshot.json.truncated) {
14550
- output += "\u26A0\uFE0F Snapshot content was truncated (too many elements in DOM)\n";
14625
+ output += " [DOM truncated]";
14551
14626
  }
14552
- output += "\n";
14627
+ output += "\n\n";
14553
14628
  output += displayLines.join("\n");
14554
14629
  if (truncated) {
14555
14630
  output += `
14556
14631
 
14557
- ... and ${lines.length - maxLines} more lines (use maxLines parameter to see more)`;
14632
+ [+${lines.length - maxLines} lines, use maxLines to see more]`;
14558
14633
  }
14559
14634
  return successResponse(output);
14560
14635
  } catch (error) {
@@ -14577,20 +14652,9 @@ async function handleResolveUidToSelector(args2) {
14577
14652
  const firefox3 = await getFirefox2();
14578
14653
  try {
14579
14654
  const selector = firefox3.resolveUidToSelector(uid);
14580
- return successResponse(`CSS Selector for UID "${uid}":
14581
-
14582
- ${selector}`);
14655
+ return successResponse(`${uid} \u2192 ${selector}`);
14583
14656
  } catch (error) {
14584
- const errorMsg = error.message;
14585
- if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID") || errorMsg.includes("not found")) {
14586
- throw new Error(
14587
- `UID "${uid}" is from an old snapshot or not found.
14588
-
14589
- The page structure may have changed since the snapshot was taken.
14590
- Please call take_snapshot to get fresh UIDs and try again.`
14591
- );
14592
- }
14593
- throw error;
14657
+ throw handleUidError(error, uid);
14594
14658
  }
14595
14659
  } catch (error) {
14596
14660
  return errorResponse(error);
@@ -14601,53 +14665,52 @@ async function handleClearSnapshot(_args) {
14601
14665
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
14602
14666
  const firefox3 = await getFirefox2();
14603
14667
  firefox3.clearSnapshot();
14604
- return successResponse(
14605
- "\u{1F9F9} Snapshot cache cleared.\n\nFor the next UID-dependent action, take a fresh snapshot first."
14606
- );
14668
+ return successResponse("\u{1F9F9} Snapshot cleared");
14607
14669
  } catch (error) {
14608
14670
  return errorResponse(error);
14609
14671
  }
14610
14672
  }
14611
- var MAX_SNAPSHOT_LINES, takeSnapshotTool, resolveUidToSelectorTool, clearSnapshotTool;
14673
+ var DEFAULT_SNAPSHOT_LINES, takeSnapshotTool, resolveUidToSelectorTool, clearSnapshotTool;
14612
14674
  var init_snapshot2 = __esm({
14613
14675
  "src/tools/snapshot.ts"() {
14614
14676
  "use strict";
14615
14677
  init_response_helpers();
14616
- MAX_SNAPSHOT_LINES = 100;
14678
+ init_uid_helpers();
14679
+ DEFAULT_SNAPSHOT_LINES = 100;
14617
14680
  takeSnapshotTool = {
14618
14681
  name: "take_snapshot",
14619
- description: "Capture a textual page snapshot with stable UIDs for elements. Always take a fresh snapshot after navigation or major DOM changes. TIP: Use the UIDs with click_by_uid / fill_by_uid / hover_by_uid. The output may be truncated for readability.",
14682
+ description: "Capture DOM snapshot with stable UIDs. Retake after navigation.",
14620
14683
  inputSchema: {
14621
14684
  type: "object",
14622
14685
  properties: {
14623
14686
  maxLines: {
14624
14687
  type: "number",
14625
- description: "Maximum number of lines to return in output (default: 100)"
14688
+ description: "Max lines (default: 100)"
14626
14689
  },
14627
14690
  includeAttributes: {
14628
14691
  type: "boolean",
14629
- description: "Include detailed ARIA and computed attributes in output (default: false)"
14692
+ description: "Include ARIA attributes (default: false)"
14630
14693
  },
14631
14694
  includeText: {
14632
14695
  type: "boolean",
14633
- description: "Include text content in output (default: true)"
14696
+ description: "Include text (default: true)"
14634
14697
  },
14635
14698
  maxDepth: {
14636
14699
  type: "number",
14637
- description: "Maximum depth of tree to include (default: unlimited)"
14700
+ description: "Max tree depth"
14638
14701
  }
14639
14702
  }
14640
14703
  }
14641
14704
  };
14642
14705
  resolveUidToSelectorTool = {
14643
14706
  name: "resolve_uid_to_selector",
14644
- description: "Resolve a UID to a CSS selector (debugging aid). Fails on stale UIDs\u2014take a fresh snapshot first.",
14707
+ description: "Resolve UID to CSS selector. Fails if stale.",
14645
14708
  inputSchema: {
14646
14709
  type: "object",
14647
14710
  properties: {
14648
14711
  uid: {
14649
14712
  type: "string",
14650
- description: "The UID from a snapshot to resolve"
14713
+ description: "UID from snapshot"
14651
14714
  }
14652
14715
  },
14653
14716
  required: ["uid"]
@@ -14655,7 +14718,7 @@ var init_snapshot2 = __esm({
14655
14718
  };
14656
14719
  clearSnapshotTool = {
14657
14720
  name: "clear_snapshot",
14658
- description: "Clear the snapshot/UID cache. Usually not needed, as navigation invalidates snapshots automatically.",
14721
+ description: "Clear snapshot cache. Usually not needed.",
14659
14722
  inputSchema: {
14660
14723
  type: "object",
14661
14724
  properties: {}
@@ -14665,18 +14728,6 @@ var init_snapshot2 = __esm({
14665
14728
  });
14666
14729
 
14667
14730
  // src/tools/input.ts
14668
- function handleUidError(error, uid) {
14669
- const errorMsg = error.message;
14670
- if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID") || errorMsg.includes("not found")) {
14671
- return new Error(
14672
- `UID "${uid}" is stale or invalid.
14673
-
14674
- The page may have changed since the snapshot was taken.
14675
- Please call take_snapshot to get fresh UIDs and try again.`
14676
- );
14677
- }
14678
- return error;
14679
- }
14680
14731
  async function handleClickByUid(args2) {
14681
14732
  try {
14682
14733
  const { uid, dblClick } = args2;
@@ -14687,9 +14738,7 @@ async function handleClickByUid(args2) {
14687
14738
  const firefox3 = await getFirefox2();
14688
14739
  try {
14689
14740
  await firefox3.clickByUid(uid, dblClick);
14690
- return successResponse(
14691
- `\u2705 ${dblClick ? "Double-clicked" : "Clicked"} element with UID "${uid}"`
14692
- );
14741
+ return successResponse(`\u2705 ${dblClick ? "dblclick" : "click"} ${uid}`);
14693
14742
  } catch (error) {
14694
14743
  throw handleUidError(error, uid);
14695
14744
  }
@@ -14707,7 +14756,7 @@ async function handleHoverByUid(args2) {
14707
14756
  const firefox3 = await getFirefox2();
14708
14757
  try {
14709
14758
  await firefox3.hoverByUid(uid);
14710
- return successResponse(`\u2705 Hovered over element with UID "${uid}"`);
14759
+ return successResponse(`\u2705 hover ${uid}`);
14711
14760
  } catch (error) {
14712
14761
  throw handleUidError(error, uid);
14713
14762
  }
@@ -14728,10 +14777,7 @@ async function handleFillByUid(args2) {
14728
14777
  const firefox3 = await getFirefox2();
14729
14778
  try {
14730
14779
  await firefox3.fillByUid(uid, value);
14731
- return successResponse(
14732
- `\u2705 Filled element with UID "${uid}"
14733
- Value: ${value.substring(0, 50)}${value.length > 50 ? "..." : ""}`
14734
- );
14780
+ return successResponse(`\u2705 fill ${uid}`);
14735
14781
  } catch (error) {
14736
14782
  throw handleUidError(error, uid);
14737
14783
  }
@@ -14752,16 +14798,11 @@ async function handleDragByUidToUid(args2) {
14752
14798
  const firefox3 = await getFirefox2();
14753
14799
  try {
14754
14800
  await firefox3.dragByUidToUid(fromUid, toUid);
14755
- return successResponse(`\u2705 Dragged element "${fromUid}" to "${toUid}"`);
14801
+ return successResponse(`\u2705 drag ${fromUid}\u2192${toUid}`);
14756
14802
  } catch (error) {
14757
14803
  const errorMsg = error.message;
14758
14804
  if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID")) {
14759
- throw new Error(
14760
- `One or both UIDs (from: "${fromUid}", to: "${toUid}") are stale or invalid.
14761
-
14762
- The page may have changed since the snapshot was taken.
14763
- Please call take_snapshot to get fresh UIDs and try again.`
14764
- );
14805
+ throw new Error(`UIDs stale/invalid. Call take_snapshot first.`);
14765
14806
  }
14766
14807
  throw error;
14767
14808
  }
@@ -14787,21 +14828,11 @@ async function handleFillFormByUid(args2) {
14787
14828
  const firefox3 = await getFirefox2();
14788
14829
  try {
14789
14830
  await firefox3.fillFormByUid(elements);
14790
- return successResponse(
14791
- `\u2705 Filled ${elements.length} form field(s):
14792
- ` + elements.map(
14793
- (el) => ` - ${el.uid}: ${el.value.substring(0, 30)}${el.value.length > 30 ? "..." : ""}`
14794
- ).join("\n")
14795
- );
14831
+ return successResponse(`\u2705 filled ${elements.length} fields`);
14796
14832
  } catch (error) {
14797
14833
  const errorMsg = error.message;
14798
14834
  if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID")) {
14799
- throw new Error(
14800
- `One or more UIDs are stale or invalid.
14801
-
14802
- The page may have changed since the snapshot was taken.
14803
- Please call take_snapshot to get fresh UIDs and try again.`
14804
- );
14835
+ throw new Error(`UIDs stale/invalid. Call take_snapshot first.`);
14805
14836
  }
14806
14837
  throw error;
14807
14838
  }
@@ -14822,26 +14853,17 @@ async function handleUploadFileByUid(args2) {
14822
14853
  const firefox3 = await getFirefox2();
14823
14854
  try {
14824
14855
  await firefox3.uploadFileByUid(uid, filePath);
14825
- return successResponse(`\u2705 Uploaded file to element with UID "${uid}"
14826
- File: ${filePath}`);
14856
+ return successResponse(`\u2705 upload ${uid}`);
14827
14857
  } catch (error) {
14828
14858
  const errorMsg = error.message;
14829
14859
  if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID")) {
14830
14860
  throw handleUidError(error, uid);
14831
14861
  }
14832
14862
  if (errorMsg.includes("not a file input") || errorMsg.includes('type="file"')) {
14833
- throw new Error(
14834
- `Element with UID "${uid}" is not an <input type="file"> element.
14835
-
14836
- Please ensure the UID points to a file input element.`
14837
- );
14863
+ throw new Error(`${uid} is not a file input`);
14838
14864
  }
14839
14865
  if (errorMsg.includes("hidden") || errorMsg.includes("not visible")) {
14840
- throw new Error(
14841
- `File input element with UID "${uid}" is hidden or not interactable.
14842
-
14843
- Some file inputs are hidden and cannot be directly interacted with.`
14844
- );
14866
+ throw new Error(`${uid} is hidden/not interactable`);
14845
14867
  }
14846
14868
  throw error;
14847
14869
  }
@@ -14854,19 +14876,20 @@ var init_input = __esm({
14854
14876
  "src/tools/input.ts"() {
14855
14877
  "use strict";
14856
14878
  init_response_helpers();
14879
+ init_uid_helpers();
14857
14880
  clickByUidTool = {
14858
14881
  name: "click_by_uid",
14859
- description: "Click an element identified by its UID. Supports double-click via dblClick=true. TIP: Take a fresh snapshot if the UID becomes stale.",
14882
+ description: "Click element by UID. Set dblClick for double-click.",
14860
14883
  inputSchema: {
14861
14884
  type: "object",
14862
14885
  properties: {
14863
14886
  uid: {
14864
14887
  type: "string",
14865
- description: "The UID of the element to click"
14888
+ description: "Element UID from snapshot"
14866
14889
  },
14867
14890
  dblClick: {
14868
14891
  type: "boolean",
14869
- description: "If true, performs a double-click (default: false)"
14892
+ description: "Double-click (default: false)"
14870
14893
  }
14871
14894
  },
14872
14895
  required: ["uid"]
@@ -14874,13 +14897,13 @@ var init_input = __esm({
14874
14897
  };
14875
14898
  hoverByUidTool = {
14876
14899
  name: "hover_by_uid",
14877
- description: "Hover over an element identified by its UID. TIP: Take a fresh snapshot if the UID becomes stale.",
14900
+ description: "Hover over element by UID.",
14878
14901
  inputSchema: {
14879
14902
  type: "object",
14880
14903
  properties: {
14881
14904
  uid: {
14882
14905
  type: "string",
14883
- description: "The UID of the element to hover over"
14906
+ description: "Element UID from snapshot"
14884
14907
  }
14885
14908
  },
14886
14909
  required: ["uid"]
@@ -14888,17 +14911,17 @@ var init_input = __esm({
14888
14911
  };
14889
14912
  fillByUidTool = {
14890
14913
  name: "fill_by_uid",
14891
- description: "Fill a text input or textarea identified by its UID. Keep values short and safe. TIP: Take a fresh snapshot if the UID becomes stale.",
14914
+ description: "Fill text input/textarea by UID.",
14892
14915
  inputSchema: {
14893
14916
  type: "object",
14894
14917
  properties: {
14895
14918
  uid: {
14896
14919
  type: "string",
14897
- description: "The UID of the input element"
14920
+ description: "Input element UID from snapshot"
14898
14921
  },
14899
14922
  value: {
14900
14923
  type: "string",
14901
- description: "The text value to fill into the input"
14924
+ description: "Text to fill"
14902
14925
  }
14903
14926
  },
14904
14927
  required: ["uid", "value"]
@@ -14906,17 +14929,17 @@ var init_input = __esm({
14906
14929
  };
14907
14930
  dragByUidToUidTool = {
14908
14931
  name: "drag_by_uid_to_uid",
14909
- description: "Simulate HTML5 drag-and-drop from one UID to another using JS drag events. May not work with all custom libraries.",
14932
+ description: "Drag element to another (HTML5 drag events).",
14910
14933
  inputSchema: {
14911
14934
  type: "object",
14912
14935
  properties: {
14913
14936
  fromUid: {
14914
14937
  type: "string",
14915
- description: "The UID of the element to drag"
14938
+ description: "Source element UID"
14916
14939
  },
14917
14940
  toUid: {
14918
14941
  type: "string",
14919
- description: "The UID of the target element to drop onto"
14942
+ description: "Target element UID"
14920
14943
  }
14921
14944
  },
14922
14945
  required: ["fromUid", "toUid"]
@@ -14924,23 +14947,23 @@ var init_input = __esm({
14924
14947
  };
14925
14948
  fillFormByUidTool = {
14926
14949
  name: "fill_form_by_uid",
14927
- description: "Fill multiple form fields in one call using an array of {uid, value} pairs.",
14950
+ description: "Fill multiple form fields at once.",
14928
14951
  inputSchema: {
14929
14952
  type: "object",
14930
14953
  properties: {
14931
14954
  elements: {
14932
14955
  type: "array",
14933
- description: "Array of form field UIDs with their values",
14956
+ description: "Array of {uid, value} pairs",
14934
14957
  items: {
14935
14958
  type: "object",
14936
14959
  properties: {
14937
14960
  uid: {
14938
14961
  type: "string",
14939
- description: "The UID of the form field"
14962
+ description: "Field UID"
14940
14963
  },
14941
14964
  value: {
14942
14965
  type: "string",
14943
- description: "The value to fill"
14966
+ description: "Field value"
14944
14967
  }
14945
14968
  },
14946
14969
  required: ["uid", "value"]
@@ -14952,17 +14975,17 @@ var init_input = __esm({
14952
14975
  };
14953
14976
  uploadFileByUidTool = {
14954
14977
  name: "upload_file_by_uid",
14955
- description: 'Upload a file into an <input type="file"> element identified by its UID. The file path must be accessible to the server.',
14978
+ description: "Upload file to file input by UID.",
14956
14979
  inputSchema: {
14957
14980
  type: "object",
14958
14981
  properties: {
14959
14982
  uid: {
14960
14983
  type: "string",
14961
- description: "The UID of the file input element"
14984
+ description: "File input UID from snapshot"
14962
14985
  },
14963
14986
  filePath: {
14964
14987
  type: "string",
14965
- description: "Local filesystem path to the file to upload"
14988
+ description: "Local file path"
14966
14989
  }
14967
14990
  },
14968
14991
  required: ["uid", "filePath"]
@@ -14972,60 +14995,46 @@ var init_input = __esm({
14972
14995
  });
14973
14996
 
14974
14997
  // src/tools/screenshot.ts
14998
+ function buildScreenshotResponse(base64Png, label) {
14999
+ const sizeKB = Math.round(base64Png.length / 1024);
15000
+ if (base64Png.length > TOKEN_LIMITS.MAX_SCREENSHOT_CHARS) {
15001
+ const truncatedData = base64Png.slice(0, TOKEN_LIMITS.MAX_SCREENSHOT_CHARS);
15002
+ return successResponse(`\u{1F4F8} ${label} (${sizeKB}KB) [truncated]
15003
+ ${truncatedData}`);
15004
+ }
15005
+ const warn = base64Png.length > TOKEN_LIMITS.WARNING_THRESHOLD_CHARS ? " [large]" : "";
15006
+ return successResponse(`\u{1F4F8} ${label} (${sizeKB}KB)${warn}
15007
+ ${base64Png}`);
15008
+ }
14975
15009
  async function handleScreenshotPage(_args) {
14976
15010
  try {
14977
15011
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
14978
15012
  const firefox3 = await getFirefox2();
14979
15013
  const base64Png = await firefox3.takeScreenshotPage();
14980
15014
  if (!base64Png || typeof base64Png !== "string") {
14981
- throw new Error("Failed to capture screenshot: invalid data returned");
15015
+ throw new Error("Invalid screenshot data");
14982
15016
  }
14983
- return successResponse(
14984
- `\u{1F4F8} Page screenshot captured (${Math.round(base64Png.length / 1024)}KB)
14985
-
14986
- Base64 PNG data:
14987
- ${base64Png}`
14988
- );
15017
+ return buildScreenshotResponse(base64Png, "page");
14989
15018
  } catch (error) {
14990
- return errorResponse(
14991
- new Error(
14992
- `Failed to capture page screenshot: ${error.message}
14993
-
14994
- The page may not be fully loaded or accessible.`
14995
- )
14996
- );
15019
+ return errorResponse(error);
14997
15020
  }
14998
15021
  }
14999
15022
  async function handleScreenshotByUid(args2) {
15000
15023
  try {
15001
15024
  const { uid } = args2;
15002
15025
  if (!uid || typeof uid !== "string") {
15003
- throw new Error("uid parameter is required and must be a string");
15026
+ throw new Error("uid required");
15004
15027
  }
15005
15028
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
15006
15029
  const firefox3 = await getFirefox2();
15007
15030
  try {
15008
15031
  const base64Png = await firefox3.takeScreenshotByUid(uid);
15009
15032
  if (!base64Png || typeof base64Png !== "string") {
15010
- throw new Error("Failed to capture screenshot: invalid data returned");
15033
+ throw new Error("Invalid screenshot data");
15011
15034
  }
15012
- return successResponse(
15013
- `\u{1F4F8} Element screenshot captured for UID "${uid}" (${Math.round(base64Png.length / 1024)}KB)
15014
-
15015
- Base64 PNG data:
15016
- ${base64Png}`
15017
- );
15035
+ return buildScreenshotResponse(base64Png, uid);
15018
15036
  } catch (error) {
15019
- const errorMsg = error.message;
15020
- if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID") || errorMsg.includes("not found")) {
15021
- throw new Error(
15022
- `UID "${uid}" is stale or invalid.
15023
-
15024
- The page may have changed since the snapshot was taken.
15025
- Please call take_snapshot to get fresh UIDs and try again.`
15026
- );
15027
- }
15028
- throw error;
15037
+ throw handleUidError(error, uid);
15029
15038
  }
15030
15039
  } catch (error) {
15031
15040
  return errorResponse(error);
@@ -15036,9 +15045,10 @@ var init_screenshot = __esm({
15036
15045
  "src/tools/screenshot.ts"() {
15037
15046
  "use strict";
15038
15047
  init_response_helpers();
15048
+ init_uid_helpers();
15039
15049
  screenshotPageTool = {
15040
15050
  name: "screenshot_page",
15041
- description: "Capture a PNG screenshot of the current page and return it as a base64 string (without data: prefix). TIP: Use for visual verification rather than structural inspection.",
15051
+ description: "Capture page screenshot as base64 PNG.",
15042
15052
  inputSchema: {
15043
15053
  type: "object",
15044
15054
  properties: {}
@@ -15046,13 +15056,13 @@ var init_screenshot = __esm({
15046
15056
  };
15047
15057
  screenshotByUidTool = {
15048
15058
  name: "screenshot_by_uid",
15049
- description: "Capture a PNG screenshot of a specific element by UID and return it as a base64 string (without data: prefix). TIP: Take a fresh snapshot if the UID is stale.",
15059
+ description: "Capture element screenshot by UID as base64 PNG.",
15050
15060
  inputSchema: {
15051
15061
  type: "object",
15052
15062
  properties: {
15053
15063
  uid: {
15054
15064
  type: "string",
15055
- description: "The UID of the element to screenshot"
15065
+ description: "Element UID from snapshot"
15056
15066
  }
15057
15067
  },
15058
15068
  required: ["uid"]
@@ -15069,17 +15079,11 @@ async function handleAcceptDialog(args2) {
15069
15079
  const firefox3 = await getFirefox2();
15070
15080
  try {
15071
15081
  await firefox3.acceptDialog(promptText);
15072
- let message = "\u2705 Dialog accepted";
15073
- if (promptText) {
15074
- message += ` with text: "${promptText}"`;
15075
- }
15076
- return successResponse(message);
15082
+ return successResponse(promptText ? `\u2705 Accepted: "${promptText}"` : "\u2705 Accepted");
15077
15083
  } catch (error) {
15078
15084
  const errorMsg = error.message;
15079
15085
  if (errorMsg.includes("no such alert") || errorMsg.includes("No dialog")) {
15080
- throw new Error(
15081
- "No active dialog found.\n\nDialogs must be accepted shortly after they appear. Make sure a dialog is currently visible on the page."
15082
- );
15086
+ throw new Error("No active dialog");
15083
15087
  }
15084
15088
  throw error;
15085
15089
  }
@@ -15093,13 +15097,11 @@ async function handleDismissDialog(_args) {
15093
15097
  const firefox3 = await getFirefox2();
15094
15098
  try {
15095
15099
  await firefox3.dismissDialog();
15096
- return successResponse("\u2705 Dialog dismissed/cancelled");
15100
+ return successResponse("\u2705 Dismissed");
15097
15101
  } catch (error) {
15098
15102
  const errorMsg = error.message;
15099
15103
  if (errorMsg.includes("no such alert") || errorMsg.includes("No dialog")) {
15100
- throw new Error(
15101
- "No active dialog found.\n\nDialogs must be dismissed shortly after they appear. Make sure a dialog is currently visible on the page."
15102
- );
15104
+ throw new Error("No active dialog");
15103
15105
  }
15104
15106
  throw error;
15105
15107
  }
@@ -15120,19 +15122,9 @@ async function handleNavigateHistory(args2) {
15120
15122
  } else {
15121
15123
  await firefox3.navigateForward();
15122
15124
  }
15123
- return successResponse(
15124
- `\u2705 Navigated ${direction} in history
15125
-
15126
- \u26A0\uFE0F UIDs from previous snapshots are now stale. Call take_snapshot before using UID-based actions.`
15127
- );
15125
+ return successResponse(`\u2705 ${direction}`);
15128
15126
  } catch (error) {
15129
- return errorResponse(
15130
- new Error(
15131
- `Failed to navigate ${args2.direction || "in history"}: ${error.message}
15132
-
15133
- The page may not have history in this direction available.`
15134
- )
15135
- );
15127
+ return errorResponse(error);
15136
15128
  }
15137
15129
  }
15138
15130
  async function handleSetViewportSize(args2) {
@@ -15147,19 +15139,9 @@ async function handleSetViewportSize(args2) {
15147
15139
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
15148
15140
  const firefox3 = await getFirefox2();
15149
15141
  await firefox3.setViewportSize(width, height);
15150
- return successResponse(
15151
- `\u2705 Viewport size set to ${width}x${height} pixels
15152
-
15153
- NOTE: Actual viewport may differ slightly in some browser modes (e.g., headless).`
15154
- );
15142
+ return successResponse(`\u2705 ${width}x${height}`);
15155
15143
  } catch (error) {
15156
- return errorResponse(
15157
- new Error(
15158
- `Failed to set viewport size: ${error.message}
15159
-
15160
- Some browser configurations may not support precise viewport sizing.`
15161
- )
15162
- );
15144
+ return errorResponse(error);
15163
15145
  }
15164
15146
  }
15165
15147
  var acceptDialogTool, dismissDialogTool, navigateHistoryTool, setViewportSizeTool;
@@ -15169,20 +15151,20 @@ var init_utilities = __esm({
15169
15151
  init_response_helpers();
15170
15152
  acceptDialogTool = {
15171
15153
  name: "accept_dialog",
15172
- description: "Accept the active browser dialog (alert/confirm/prompt). For prompts, you may provide promptText. Returns an error if no dialog is open.",
15154
+ description: "Accept browser dialog. Provide promptText for prompts.",
15173
15155
  inputSchema: {
15174
15156
  type: "object",
15175
15157
  properties: {
15176
15158
  promptText: {
15177
15159
  type: "string",
15178
- description: "Text to enter in a prompt dialog (optional, only for prompt dialogs)"
15160
+ description: "Text for prompt dialogs"
15179
15161
  }
15180
15162
  }
15181
15163
  }
15182
15164
  };
15183
15165
  dismissDialogTool = {
15184
15166
  name: "dismiss_dialog",
15185
- description: "Dismiss the active browser dialog (alert/confirm/prompt). Returns an error if no dialog is open.",
15167
+ description: "Dismiss browser dialog.",
15186
15168
  inputSchema: {
15187
15169
  type: "object",
15188
15170
  properties: {}
@@ -15190,14 +15172,14 @@ var init_utilities = __esm({
15190
15172
  };
15191
15173
  navigateHistoryTool = {
15192
15174
  name: "navigate_history",
15193
- description: "Navigate the selected tab's history back or forward. NOTE: After navigation, UIDs from previous snapshots are stale\u2014take a new snapshot before UID-based actions.",
15175
+ description: "Navigate history back/forward. UIDs become stale.",
15194
15176
  inputSchema: {
15195
15177
  type: "object",
15196
15178
  properties: {
15197
15179
  direction: {
15198
15180
  type: "string",
15199
15181
  enum: ["back", "forward"],
15200
- description: "Direction to navigate in history"
15182
+ description: "back or forward"
15201
15183
  }
15202
15184
  },
15203
15185
  required: ["direction"]
@@ -15205,17 +15187,17 @@ var init_utilities = __esm({
15205
15187
  };
15206
15188
  setViewportSizeTool = {
15207
15189
  name: "set_viewport_size",
15208
- description: "Set the browser viewport size (width x height in pixels). In some modes (e.g., headless), the actual size may vary slightly.",
15190
+ description: "Set viewport dimensions in pixels.",
15209
15191
  inputSchema: {
15210
15192
  type: "object",
15211
15193
  properties: {
15212
15194
  width: {
15213
15195
  type: "number",
15214
- description: "Viewport width in pixels"
15196
+ description: "Width in pixels"
15215
15197
  },
15216
15198
  height: {
15217
15199
  type: "number",
15218
- description: "Viewport height in pixels"
15200
+ description: "Height in pixels"
15219
15201
  }
15220
15202
  },
15221
15203
  required: ["width", "height"]
@@ -15238,6 +15220,33 @@ var init_tools = __esm({
15238
15220
  }
15239
15221
  });
15240
15222
 
15223
+ // src/utils/errors.ts
15224
+ function isDisconnectionError(error) {
15225
+ if (error instanceof FirefoxDisconnectedError) {
15226
+ return true;
15227
+ }
15228
+ if (error instanceof Error) {
15229
+ const message = error.message.toLowerCase();
15230
+ return message.includes("session deleted") || message.includes("session not created") || message.includes("no such window") || message.includes("no such session") || message.includes("target window already closed") || message.includes("unable to connect") || message.includes("connection refused") || message.includes("not connected") || message.includes("driver not connected") || message.includes("invalid session id") || message.includes("browsing context has been discarded");
15231
+ }
15232
+ return false;
15233
+ }
15234
+ var FirefoxDisconnectedError;
15235
+ var init_errors2 = __esm({
15236
+ "src/utils/errors.ts"() {
15237
+ "use strict";
15238
+ FirefoxDisconnectedError = class extends Error {
15239
+ constructor(reason) {
15240
+ const baseMessage = "Firefox browser is not connected";
15241
+ const instruction = "The Firefox browser window was closed by the user. To continue browser automation, ask the user to restart the firefox-devtools-mcp server (they need to restart Claude Code or the MCP connection). This will launch a new Firefox instance.";
15242
+ const fullMessage = reason ? `${baseMessage}: ${reason}. ${instruction}` : `${baseMessage}. ${instruction}`;
15243
+ super(fullMessage);
15244
+ this.name = "FirefoxDisconnectedError";
15245
+ }
15246
+ };
15247
+ }
15248
+ });
15249
+
15241
15250
  // node_modules/dotenv/package.json
15242
15251
  var require_package = __commonJS({
15243
15252
  "node_modules/dotenv/package.json"(exports, module) {
@@ -15640,28 +15649,46 @@ var require_main = __commonJS({
15640
15649
  var index_exports = {};
15641
15650
  __export(index_exports, {
15642
15651
  FirefoxDevTools: () => FirefoxClient,
15652
+ FirefoxDisconnectedError: () => FirefoxDisconnectedError,
15643
15653
  args: () => args,
15644
- getFirefox: () => getFirefox
15654
+ getFirefox: () => getFirefox,
15655
+ isDisconnectionError: () => isDisconnectionError,
15656
+ resetFirefox: () => resetFirefox
15645
15657
  });
15646
15658
  import { version } from "process";
15647
15659
  import { fileURLToPath as fileURLToPath2 } from "url";
15648
15660
  import { resolve as resolve2 } from "path";
15649
15661
  import { realpathSync } from "fs";
15650
- async function getFirefox() {
15651
- if (!firefox2) {
15652
- log("Initializing Firefox DevTools connection...");
15653
- const options = {
15654
- firefoxPath: args.firefoxPath ?? void 0,
15655
- headless: args.headless,
15656
- profilePath: args.profilePath ?? void 0,
15657
- viewport: args.viewport ?? void 0,
15658
- args: args.firefoxArg ?? void 0,
15659
- startUrl: args.startUrl ?? void 0
15660
- };
15661
- firefox2 = new FirefoxClient(options);
15662
- await firefox2.connect();
15663
- log("Firefox DevTools connection established");
15662
+ function resetFirefox() {
15663
+ if (firefox2) {
15664
+ firefox2.reset();
15665
+ firefox2 = null;
15664
15666
  }
15667
+ log("Firefox instance reset - will reconnect on next tool call");
15668
+ }
15669
+ async function getFirefox() {
15670
+ if (firefox2) {
15671
+ const isConnected = await firefox2.isConnected();
15672
+ if (!isConnected) {
15673
+ log("Firefox connection lost - browser was closed or disconnected");
15674
+ resetFirefox();
15675
+ throw new FirefoxDisconnectedError("Browser was closed");
15676
+ }
15677
+ return firefox2;
15678
+ }
15679
+ log("Initializing Firefox DevTools connection...");
15680
+ const options = {
15681
+ firefoxPath: args.firefoxPath ?? void 0,
15682
+ headless: args.headless,
15683
+ profilePath: args.profilePath ?? void 0,
15684
+ viewport: args.viewport ?? void 0,
15685
+ args: args.firefoxArg ?? void 0,
15686
+ startUrl: args.startUrl ?? void 0,
15687
+ acceptInsecureCerts: args.acceptInsecureCerts
15688
+ };
15689
+ firefox2 = new FirefoxClient(options);
15690
+ await firefox2.connect();
15691
+ log("Firefox DevTools connection established");
15665
15692
  return firefox2;
15666
15693
  }
15667
15694
  async function main() {
@@ -15729,7 +15756,9 @@ var init_index = __esm({
15729
15756
  init_cli();
15730
15757
  init_firefox();
15731
15758
  init_tools();
15759
+ init_errors2();
15732
15760
  init_firefox();
15761
+ init_errors2();
15733
15762
  if (process.env.NODE_ENV !== "production") {
15734
15763
  try {
15735
15764
  const { config } = await Promise.resolve().then(() => __toESM(require_main(), 1));
@@ -15829,8 +15858,11 @@ var init_index = __esm({
15829
15858
  await init_index();
15830
15859
  export {
15831
15860
  FirefoxClient as FirefoxDevTools,
15861
+ FirefoxDisconnectedError,
15832
15862
  args,
15833
- getFirefox
15863
+ getFirefox,
15864
+ isDisconnectionError,
15865
+ resetFirefox
15834
15866
  };
15835
15867
  /*! Bundled license information:
15836
15868