firefox-devtools-mcp 0.4.0 → 0.5.2

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 (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +201 -341
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -128,3 +128,7 @@ npm run inspector:dev
128
128
  ## CI and Release
129
129
 
130
130
  - GitHub Actions for CI, Release, and npm publish are included. See docs/ci-and-release.md for details and required secrets.
131
+
132
+ ## Author
133
+
134
+ Created by [Tomáš Grasl](https://www.tomasgrasl.cz/)
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();
@@ -12579,10 +12582,42 @@ var init_network = __esm({
12579
12582
  */
12580
12583
  parseHeaders(headers) {
12581
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
+ };
12582
12612
  if (Array.isArray(headers)) {
12583
12613
  for (const h of headers) {
12584
- if (h.name && h.value) {
12585
- 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;
12586
12621
  }
12587
12622
  }
12588
12623
  }
@@ -13157,7 +13192,7 @@ var MAX_ATTR_LENGTH;
13157
13192
  var init_formatter = __esm({
13158
13193
  "src/firefox/snapshot/formatter.ts"() {
13159
13194
  "use strict";
13160
- MAX_ATTR_LENGTH = 50;
13195
+ MAX_ATTR_LENGTH = 30;
13161
13196
  }
13162
13197
  });
13163
13198
 
@@ -13784,9 +13819,6 @@ var init_firefox = __esm({
13784
13819
  });
13785
13820
 
13786
13821
  // src/utils/response-helpers.ts
13787
- function estimateTokens(text) {
13788
- return Math.ceil(text.length / 4);
13789
- }
13790
13822
  function truncateText(text, maxChars, suffix = "\n\n[... truncated - exceeded size limit]") {
13791
13823
  if (text.length <= maxChars) {
13792
13824
  return text;
@@ -13868,18 +13900,15 @@ var init_response_helpers = __esm({
13868
13900
 
13869
13901
  // src/tools/pages.ts
13870
13902
  function formatPageList(tabs, selectedIdx) {
13871
- const lines = [`\u{1F4C4} Open pages (${tabs.length} total, selected: [${selectedIdx}]):`];
13872
13903
  if (tabs.length === 0) {
13873
- lines.push(" (no pages open)");
13874
- } else {
13875
- for (const tab of tabs) {
13876
- const idx = tabs.indexOf(tab);
13877
- const indicator = idx === selectedIdx ? "\u{1F449}" : " ";
13878
- const title = tab.title || "Untitled";
13879
- const url = tab.url || "about:blank";
13880
- lines.push(`${indicator} [${idx}] ${title}`);
13881
- lines.push(` ${url}`);
13882
- }
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}`);
13883
13912
  }
13884
13913
  return lines.join("\n");
13885
13914
  }
@@ -13904,14 +13933,7 @@ async function handleNewPage(args2) {
13904
13933
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
13905
13934
  const firefox3 = await getFirefox2();
13906
13935
  const newIdx = await firefox3.createNewPage(url);
13907
- await firefox3.refreshTabs();
13908
- const tabs = firefox3.getTabs();
13909
- const newTab = tabs[newIdx];
13910
- return successResponse(
13911
- `\u2705 Created new page [${newIdx}] and navigated to: ${url}
13912
- Title: ${newTab?.title || "Loading..."}
13913
- Total pages: ${tabs.length}`
13914
- );
13936
+ return successResponse(`\u2705 new page [${newIdx}] \u2192 ${url}`);
13915
13937
  } catch (error) {
13916
13938
  return errorResponse(error);
13917
13939
  }
@@ -13932,10 +13954,7 @@ async function handleNavigatePage(args2) {
13932
13954
  throw new Error("No page selected");
13933
13955
  }
13934
13956
  await firefox3.navigate(url);
13935
- return successResponse(
13936
- `\u2705 Navigated page [${selectedIdx}] to: ${url}
13937
- Previous URL: ${page.url}`
13938
- );
13957
+ return successResponse(`\u2705 [${selectedIdx}] \u2192 ${url}`);
13939
13958
  } catch (error) {
13940
13959
  return errorResponse(error);
13941
13960
  }
@@ -13948,47 +13967,30 @@ async function handleSelectPage(args2) {
13948
13967
  await firefox3.refreshTabs();
13949
13968
  const tabs = firefox3.getTabs();
13950
13969
  let selectedIdx;
13951
- let selectionMethod;
13952
13970
  if (typeof pageIdx === "number") {
13953
13971
  selectedIdx = pageIdx;
13954
- selectionMethod = "by index";
13955
13972
  } else if (url && typeof url === "string") {
13956
13973
  const urlLower = url.toLowerCase();
13957
13974
  const foundIdx = tabs.findIndex((tab) => tab.url?.toLowerCase().includes(urlLower));
13958
13975
  if (foundIdx === -1) {
13959
- throw new Error(
13960
- `No page found with URL matching "${url}". Use list_pages to see all available pages.`
13961
- );
13976
+ throw new Error(`No page matching URL "${url}"`);
13962
13977
  }
13963
13978
  selectedIdx = foundIdx;
13964
- selectionMethod = `by URL pattern "${url}"`;
13965
13979
  } else if (title && typeof title === "string") {
13966
13980
  const titleLower = title.toLowerCase();
13967
13981
  const foundIdx = tabs.findIndex((tab) => tab.title?.toLowerCase().includes(titleLower));
13968
13982
  if (foundIdx === -1) {
13969
- throw new Error(
13970
- `No page found with title matching "${title}". Use list_pages to see all available pages.`
13971
- );
13983
+ throw new Error(`No page matching title "${title}"`);
13972
13984
  }
13973
13985
  selectedIdx = foundIdx;
13974
- selectionMethod = `by title pattern "${title}"`;
13975
13986
  } else {
13976
- throw new Error(
13977
- "At least one of pageIdx, url, or title must be provided. Use list_pages to see available pages."
13978
- );
13987
+ throw new Error("Provide pageIdx, url, or title");
13979
13988
  }
13980
- const page = tabs[selectedIdx];
13981
- if (!page) {
13982
- throw new Error(
13983
- `Page at index ${selectedIdx} not found. Use list_pages to see valid indices.`
13984
- );
13989
+ if (!tabs[selectedIdx]) {
13990
+ throw new Error(`Page [${selectedIdx}] not found`);
13985
13991
  }
13986
13992
  await firefox3.selectTab(selectedIdx);
13987
- return successResponse(
13988
- `\u2705 Selected page [${selectedIdx}] ${selectionMethod}
13989
- Title: ${page.title || "Untitled"}
13990
- URL: ${page.url || "about:blank"}`
13991
- );
13993
+ return successResponse(`\u2705 selected [${selectedIdx}]`);
13992
13994
  } catch (error) {
13993
13995
  return errorResponse(error);
13994
13996
  }
@@ -14008,10 +14010,7 @@ async function handleClosePage(args2) {
14008
14010
  throw new Error(`Page with index ${pageIdx} not found`);
14009
14011
  }
14010
14012
  await firefox3.closeTab(pageIdx);
14011
- return successResponse(
14012
- `\u2705 Closed page [${pageIdx}]: ${pageToClose.title}
14013
- ${pageToClose.url}`
14014
- );
14013
+ return successResponse(`\u2705 closed [${pageIdx}]`);
14015
14014
  } catch (error) {
14016
14015
  return errorResponse(error);
14017
14016
  }
@@ -14023,7 +14022,7 @@ var init_pages2 = __esm({
14023
14022
  init_response_helpers();
14024
14023
  listPagesTool = {
14025
14024
  name: "list_pages",
14026
- 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.",
14027
14026
  inputSchema: {
14028
14027
  type: "object",
14029
14028
  properties: {}
@@ -14031,13 +14030,13 @@ var init_pages2 = __esm({
14031
14030
  };
14032
14031
  newPageTool = {
14033
14032
  name: "new_page",
14034
- 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.",
14035
14034
  inputSchema: {
14036
14035
  type: "object",
14037
14036
  properties: {
14038
14037
  url: {
14039
14038
  type: "string",
14040
- description: "URL to load in a new page"
14039
+ description: "Target URL"
14041
14040
  }
14042
14041
  },
14043
14042
  required: ["url"]
@@ -14045,13 +14044,13 @@ var init_pages2 = __esm({
14045
14044
  };
14046
14045
  navigatePageTool = {
14047
14046
  name: "navigate_page",
14048
- description: "Navigate the currently selected tab to the provided URL.",
14047
+ description: "Navigate selected tab to URL.",
14049
14048
  inputSchema: {
14050
14049
  type: "object",
14051
14050
  properties: {
14052
14051
  url: {
14053
14052
  type: "string",
14054
- description: "URL to navigate the page to"
14053
+ description: "Target URL"
14055
14054
  }
14056
14055
  },
14057
14056
  required: ["url"]
@@ -14059,21 +14058,21 @@ var init_pages2 = __esm({
14059
14058
  };
14060
14059
  selectPageTool = {
14061
14060
  name: "select_page",
14062
- 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.",
14063
14062
  inputSchema: {
14064
14063
  type: "object",
14065
14064
  properties: {
14066
14065
  pageIdx: {
14067
14066
  type: "number",
14068
- 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)"
14069
14068
  },
14070
14069
  url: {
14071
14070
  type: "string",
14072
- 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)"
14073
14072
  },
14074
14073
  title: {
14075
14074
  type: "string",
14076
- description: 'Select page by title (partial match, case-insensitive). Example: "Google" will match "Google Search - About"'
14075
+ description: "Title substring (case-insensitive)"
14077
14076
  }
14078
14077
  },
14079
14078
  required: []
@@ -14081,13 +14080,13 @@ var init_pages2 = __esm({
14081
14080
  };
14082
14081
  closePageTool = {
14083
14082
  name: "close_page",
14084
- description: "Close the tab at the given index. Use list_pages to find valid indices.",
14083
+ description: "Close tab by index.",
14085
14084
  inputSchema: {
14086
14085
  type: "object",
14087
14086
  properties: {
14088
14087
  pageIdx: {
14089
14088
  type: "number",
14090
- description: "The index of the page to close"
14089
+ description: "Tab index to close"
14091
14090
  }
14092
14091
  },
14093
14092
  required: ["pageIdx"]
@@ -14222,7 +14221,7 @@ Total messages: ${totalCount}${filterInfo.length > 0 ? `, Filters: ${filterInfo.
14222
14221
  }
14223
14222
  if (truncated) {
14224
14223
  output += `
14225
- ... ${filteredCount - messages.length} more messages (increase limit to see more)`;
14224
+ [+${filteredCount - messages.length} more]`;
14226
14225
  }
14227
14226
  return successResponse(output);
14228
14227
  } catch (error) {
@@ -14235,11 +14234,7 @@ async function handleClearConsoleMessages(_args) {
14235
14234
  const firefox3 = await getFirefox2();
14236
14235
  const count = (await firefox3.getConsoleMessages()).length;
14237
14236
  firefox3.clearConsoleMessages();
14238
- return successResponse(
14239
- `Cleared ${count} console message(s) from buffer.
14240
-
14241
- You can now capture fresh console output from new page activity.`
14242
- );
14237
+ return successResponse(`\u2705 cleared ${count} messages`);
14243
14238
  } catch (error) {
14244
14239
  return errorResponse(error);
14245
14240
  }
@@ -14251,42 +14246,42 @@ var init_console2 = __esm({
14251
14246
  init_response_helpers();
14252
14247
  listConsoleMessagesTool = {
14253
14248
  name: "list_console_messages",
14254
- 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.",
14255
14250
  inputSchema: {
14256
14251
  type: "object",
14257
14252
  properties: {
14258
14253
  level: {
14259
14254
  type: "string",
14260
14255
  enum: ["debug", "info", "warn", "error"],
14261
- description: "Filter by console message level"
14256
+ description: "Filter by level"
14262
14257
  },
14263
14258
  limit: {
14264
14259
  type: "number",
14265
- description: "Maximum number of messages to return (default: 50)"
14260
+ description: "Max messages (default: 50)"
14266
14261
  },
14267
14262
  sinceMs: {
14268
14263
  type: "number",
14269
- description: "Only show messages from the last N milliseconds (filters by timestamp)"
14264
+ description: "Only last N ms"
14270
14265
  },
14271
14266
  textContains: {
14272
14267
  type: "string",
14273
- description: "Filter messages by text content (case-insensitive substring match)"
14268
+ description: "Text filter (case-insensitive)"
14274
14269
  },
14275
14270
  source: {
14276
14271
  type: "string",
14277
- description: 'Filter messages by source (e.g., "console-api", "javascript", "network")'
14272
+ description: "Filter by source"
14278
14273
  },
14279
14274
  format: {
14280
14275
  type: "string",
14281
14276
  enum: ["text", "json"],
14282
- description: "Output format: text (default, human-readable) or json (structured data)"
14277
+ description: "Output format (default: text)"
14283
14278
  }
14284
14279
  }
14285
14280
  }
14286
14281
  };
14287
14282
  clearConsoleMessagesTool = {
14288
14283
  name: "clear_console_messages",
14289
- description: "Clear the collected console messages. TIP: Clear before a new measurement to keep output focused.",
14284
+ description: "Clear collected console messages.",
14290
14285
  inputSchema: {
14291
14286
  type: "object",
14292
14287
  properties: {}
@@ -14398,15 +14393,9 @@ async function handleListNetworkRequests(args2) {
14398
14393
  const statusInfo = req.status ? `[${req.status}${req.statusText ? " " + req.statusText : ""}]` : "[pending]";
14399
14394
  return `${req.id} | ${req.method} ${req.url} ${statusInfo}${req.isXHR ? " (XHR)" : ""}`;
14400
14395
  });
14401
- const summary = [
14402
- `Found ${requests.length} network request(s)${hasMore ? ` (showing first ${limit})` : ""}`,
14403
- "",
14404
- "Network Requests:",
14405
- ...formattedRequests,
14406
- "",
14407
- "TIP: Use the request ID (first column) with get_network_request for full details."
14408
- ].join("\n");
14409
- return successResponse(summary);
14396
+ const header = `\u{1F4E1} ${requests.length} requests${hasMore ? ` (limit ${limit})` : ""}
14397
+ `;
14398
+ return successResponse(header + formattedRequests.join("\n"));
14410
14399
  } else if (detail === "min") {
14411
14400
  const minData = limitedRequests.map((req) => ({
14412
14401
  id: req.id,
@@ -14419,8 +14408,7 @@ async function handleListNetworkRequests(args2) {
14419
14408
  duration: req.timings?.duration
14420
14409
  }));
14421
14410
  return successResponse(
14422
- `Found ${requests.length} requests${hasMore ? ` (showing first ${limit})` : ""}
14423
-
14411
+ `\u{1F4E1} ${requests.length} requests${hasMore ? ` (limit ${limit})` : ""}
14424
14412
  ` + JSON.stringify(minData, null, 2)
14425
14413
  );
14426
14414
  } else {
@@ -14437,15 +14425,12 @@ async function handleListNetworkRequests(args2) {
14437
14425
  responseHeaders: truncateHeaders(req.responseHeaders)
14438
14426
  }));
14439
14427
  return successResponse(
14440
- `Found ${requests.length} requests${hasMore ? ` (showing first ${limit})` : ""}
14441
-
14428
+ `\u{1F4E1} ${requests.length} requests${hasMore ? ` (limit ${limit})` : ""}
14442
14429
  ` + JSON.stringify(fullData, null, 2)
14443
14430
  );
14444
14431
  }
14445
14432
  } catch (error) {
14446
- return errorResponse(
14447
- `Failed to list network requests: ${error instanceof Error ? error.message : String(error)}`
14448
- );
14433
+ return errorResponse(error instanceof Error ? error : new Error(String(error)));
14449
14434
  }
14450
14435
  }
14451
14436
  async function handleGetNetworkRequest(args2) {
@@ -14456,9 +14441,7 @@ async function handleGetNetworkRequest(args2) {
14456
14441
  format = "text"
14457
14442
  } = args2;
14458
14443
  if (!id && !url) {
14459
- return errorResponse(
14460
- 'Either "id" or "url" parameter is required.\n\nTIP: Call list_network_requests first and use the returned ID for reliable lookup.'
14461
- );
14444
+ return errorResponse("id or url required");
14462
14445
  }
14463
14446
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
14464
14447
  const firefox3 = await getFirefox2();
@@ -14467,29 +14450,16 @@ async function handleGetNetworkRequest(args2) {
14467
14450
  if (id) {
14468
14451
  request = requests.find((req) => req.id === id);
14469
14452
  if (!request) {
14470
- return errorResponse(
14471
- `No network request found with ID: ${id}
14472
-
14473
- TIP: The request may have been cleared. Call list_network_requests to see available requests.`
14474
- );
14453
+ return errorResponse(`ID ${id} not found`);
14475
14454
  }
14476
14455
  } else if (url) {
14477
14456
  const matches = requests.filter((req) => req.url === url);
14478
14457
  if (matches.length === 0) {
14479
- return errorResponse(
14480
- `No network request found with URL: ${url}
14481
-
14482
- TIP: Use list_network_requests to see available requests.`
14483
- );
14458
+ return errorResponse(`URL not found: ${url}`);
14484
14459
  }
14485
14460
  if (matches.length > 1) {
14486
- const matchInfo = matches.map((req) => ` - ID: ${req.id} | ${req.method} [${req.status || "pending"}]`).join("\n");
14487
- return errorResponse(
14488
- `Multiple requests (${matches.length}) found with URL: ${url}
14489
-
14490
- Please use one of these IDs with the "id" parameter:
14491
- ` + matchInfo
14492
- );
14461
+ const ids = matches.map((req) => req.id).join(", ");
14462
+ return errorResponse(`Multiple matches, use id: ${ids}`);
14493
14463
  }
14494
14464
  request = matches[0];
14495
14465
  }
@@ -14512,11 +14482,9 @@ Please use one of these IDs with the "id" parameter:
14512
14482
  if (format === "json") {
14513
14483
  return jsonResponse(details);
14514
14484
  }
14515
- return successResponse("Network Request Details:\n\n" + JSON.stringify(details, null, 2));
14485
+ return successResponse(JSON.stringify(details, null, 2));
14516
14486
  } catch (error) {
14517
- return errorResponse(
14518
- `Failed to get network request: ${error instanceof Error ? error.message : String(error)}`
14519
- );
14487
+ return errorResponse(error instanceof Error ? error : new Error(String(error)));
14520
14488
  }
14521
14489
  }
14522
14490
  var listNetworkRequestsTool, getNetworkRequestTool;
@@ -14526,82 +14494,82 @@ var init_network2 = __esm({
14526
14494
  init_response_helpers();
14527
14495
  listNetworkRequestsTool = {
14528
14496
  name: "list_network_requests",
14529
- 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.",
14530
14498
  inputSchema: {
14531
14499
  type: "object",
14532
14500
  properties: {
14533
14501
  limit: {
14534
14502
  type: "number",
14535
- description: "Maximum number of requests to return (default: 50)"
14503
+ description: "Max requests (default: 50)"
14536
14504
  },
14537
14505
  sinceMs: {
14538
14506
  type: "number",
14539
- description: "Return only requests newer than N milliseconds ago"
14507
+ description: "Only last N ms"
14540
14508
  },
14541
14509
  urlContains: {
14542
14510
  type: "string",
14543
- description: "Filter requests by URL substring (case-insensitive)"
14511
+ description: "URL filter (case-insensitive)"
14544
14512
  },
14545
14513
  method: {
14546
14514
  type: "string",
14547
- description: "Filter by HTTP method (GET, POST, etc., case-insensitive)"
14515
+ description: "HTTP method filter"
14548
14516
  },
14549
14517
  status: {
14550
14518
  type: "number",
14551
- description: "Filter by exact HTTP status code"
14519
+ description: "Exact status code"
14552
14520
  },
14553
14521
  statusMin: {
14554
14522
  type: "number",
14555
- description: "Filter by minimum HTTP status code"
14523
+ description: "Min status code"
14556
14524
  },
14557
14525
  statusMax: {
14558
14526
  type: "number",
14559
- description: "Filter by maximum HTTP status code"
14527
+ description: "Max status code"
14560
14528
  },
14561
14529
  isXHR: {
14562
14530
  type: "boolean",
14563
- description: "Filter by XHR/fetch requests only"
14531
+ description: "XHR/fetch only"
14564
14532
  },
14565
14533
  resourceType: {
14566
14534
  type: "string",
14567
- description: "Filter by resource type (case-insensitive)"
14535
+ description: "Resource type filter"
14568
14536
  },
14569
14537
  sortBy: {
14570
14538
  type: "string",
14571
14539
  enum: ["timestamp", "duration", "status"],
14572
- description: "Sort requests by field (default: timestamp descending)"
14540
+ description: "Sort field (default: timestamp)"
14573
14541
  },
14574
14542
  detail: {
14575
14543
  type: "string",
14576
14544
  enum: ["summary", "min", "full"],
14577
- description: "Output detail level: summary (default), min (compact JSON), full (includes headers)"
14545
+ description: "Detail level (default: summary)"
14578
14546
  },
14579
14547
  format: {
14580
14548
  type: "string",
14581
14549
  enum: ["text", "json"],
14582
- description: "Output format: text (default, human-readable) or json (structured data)"
14550
+ description: "Output format (default: text)"
14583
14551
  }
14584
14552
  }
14585
14553
  }
14586
14554
  };
14587
14555
  getNetworkRequestTool = {
14588
14556
  name: "get_network_request",
14589
- 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.",
14590
14558
  inputSchema: {
14591
14559
  type: "object",
14592
14560
  properties: {
14593
14561
  id: {
14594
14562
  type: "string",
14595
- description: "The request ID from list_network_requests (recommended)"
14563
+ description: "Request ID from list_network_requests"
14596
14564
  },
14597
14565
  url: {
14598
14566
  type: "string",
14599
- description: "The URL of the request (fallback, may match multiple requests)"
14567
+ description: "URL fallback (may match multiple)"
14600
14568
  },
14601
14569
  format: {
14602
14570
  type: "string",
14603
14571
  enum: ["text", "json"],
14604
- description: "Output format: text (default) or json (structured data)"
14572
+ description: "Output format (default: text)"
14605
14573
  }
14606
14574
  }
14607
14575
  }
@@ -14609,6 +14577,20 @@ var init_network2 = __esm({
14609
14577
  }
14610
14578
  });
14611
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
+
14612
14594
  // src/tools/snapshot.ts
14613
14595
  async function handleTakeSnapshot(args2) {
14614
14596
  try {
@@ -14635,28 +14617,19 @@ async function handleTakeSnapshot(args2) {
14635
14617
  const lines = formattedText.split("\n");
14636
14618
  const truncated = lines.length > maxLines;
14637
14619
  const displayLines = truncated ? lines.slice(0, maxLines) : lines;
14638
- let output = "\u{1F4F8} Snapshot taken\n\n";
14620
+ let output = `\u{1F4F8} Snapshot (id=${snapshot.json.snapshotId})`;
14639
14621
  if (wasCapped) {
14640
- output += `\u26A0\uFE0F maxLines capped at ${TOKEN_LIMITS.MAX_SNAPSHOT_LINES_CAP} (requested: ${requestedMaxLines}) to prevent token overflow
14641
-
14642
- `;
14622
+ output += ` [maxLines capped: ${TOKEN_LIMITS.MAX_SNAPSHOT_LINES_CAP}]`;
14643
14623
  }
14644
- output += "\u2550\u2550\u2550 HOW TO USE THIS SNAPSHOT \u2550\u2550\u2550\n";
14645
- output += "\u2022 To interact with elements: use click_by_uid, hover_by_uid, or fill_by_uid with the UID\n";
14646
- output += "\u2022 After navigation: always call take_snapshot again (UIDs become stale)\n";
14647
- output += "\u2022 On stale UID errors: call take_snapshot \u2192 retry your action\n";
14648
- 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";
14649
- output += `Snapshot ID: ${snapshot.json.snapshotId}
14650
- `;
14651
14624
  if (snapshot.json.truncated) {
14652
- output += "\u26A0\uFE0F Snapshot content was truncated (too many elements in DOM)\n";
14625
+ output += " [DOM truncated]";
14653
14626
  }
14654
- output += "\n";
14627
+ output += "\n\n";
14655
14628
  output += displayLines.join("\n");
14656
14629
  if (truncated) {
14657
14630
  output += `
14658
14631
 
14659
- ... and ${lines.length - maxLines} more lines (use maxLines parameter to see more)`;
14632
+ [+${lines.length - maxLines} lines, use maxLines to see more]`;
14660
14633
  }
14661
14634
  return successResponse(output);
14662
14635
  } catch (error) {
@@ -14679,20 +14652,9 @@ async function handleResolveUidToSelector(args2) {
14679
14652
  const firefox3 = await getFirefox2();
14680
14653
  try {
14681
14654
  const selector = firefox3.resolveUidToSelector(uid);
14682
- return successResponse(`CSS Selector for UID "${uid}":
14683
-
14684
- ${selector}`);
14655
+ return successResponse(`${uid} \u2192 ${selector}`);
14685
14656
  } catch (error) {
14686
- const errorMsg = error.message;
14687
- if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID") || errorMsg.includes("not found")) {
14688
- throw new Error(
14689
- `UID "${uid}" is from an old snapshot or not found.
14690
-
14691
- The page structure may have changed since the snapshot was taken.
14692
- Please call take_snapshot to get fresh UIDs and try again.`
14693
- );
14694
- }
14695
- throw error;
14657
+ throw handleUidError(error, uid);
14696
14658
  }
14697
14659
  } catch (error) {
14698
14660
  return errorResponse(error);
@@ -14703,9 +14665,7 @@ async function handleClearSnapshot(_args) {
14703
14665
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
14704
14666
  const firefox3 = await getFirefox2();
14705
14667
  firefox3.clearSnapshot();
14706
- return successResponse(
14707
- "\u{1F9F9} Snapshot cache cleared.\n\nFor the next UID-dependent action, take a fresh snapshot first."
14708
- );
14668
+ return successResponse("\u{1F9F9} Snapshot cleared");
14709
14669
  } catch (error) {
14710
14670
  return errorResponse(error);
14711
14671
  }
@@ -14715,41 +14675,42 @@ var init_snapshot2 = __esm({
14715
14675
  "src/tools/snapshot.ts"() {
14716
14676
  "use strict";
14717
14677
  init_response_helpers();
14678
+ init_uid_helpers();
14718
14679
  DEFAULT_SNAPSHOT_LINES = 100;
14719
14680
  takeSnapshotTool = {
14720
14681
  name: "take_snapshot",
14721
- 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.",
14722
14683
  inputSchema: {
14723
14684
  type: "object",
14724
14685
  properties: {
14725
14686
  maxLines: {
14726
14687
  type: "number",
14727
- description: "Maximum number of lines to return in output (default: 100)"
14688
+ description: "Max lines (default: 100)"
14728
14689
  },
14729
14690
  includeAttributes: {
14730
14691
  type: "boolean",
14731
- description: "Include detailed ARIA and computed attributes in output (default: false)"
14692
+ description: "Include ARIA attributes (default: false)"
14732
14693
  },
14733
14694
  includeText: {
14734
14695
  type: "boolean",
14735
- description: "Include text content in output (default: true)"
14696
+ description: "Include text (default: true)"
14736
14697
  },
14737
14698
  maxDepth: {
14738
14699
  type: "number",
14739
- description: "Maximum depth of tree to include (default: unlimited)"
14700
+ description: "Max tree depth"
14740
14701
  }
14741
14702
  }
14742
14703
  }
14743
14704
  };
14744
14705
  resolveUidToSelectorTool = {
14745
14706
  name: "resolve_uid_to_selector",
14746
- 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.",
14747
14708
  inputSchema: {
14748
14709
  type: "object",
14749
14710
  properties: {
14750
14711
  uid: {
14751
14712
  type: "string",
14752
- description: "The UID from a snapshot to resolve"
14713
+ description: "UID from snapshot"
14753
14714
  }
14754
14715
  },
14755
14716
  required: ["uid"]
@@ -14757,7 +14718,7 @@ var init_snapshot2 = __esm({
14757
14718
  };
14758
14719
  clearSnapshotTool = {
14759
14720
  name: "clear_snapshot",
14760
- description: "Clear the snapshot/UID cache. Usually not needed, as navigation invalidates snapshots automatically.",
14721
+ description: "Clear snapshot cache. Usually not needed.",
14761
14722
  inputSchema: {
14762
14723
  type: "object",
14763
14724
  properties: {}
@@ -14767,18 +14728,6 @@ var init_snapshot2 = __esm({
14767
14728
  });
14768
14729
 
14769
14730
  // src/tools/input.ts
14770
- function handleUidError(error, uid) {
14771
- const errorMsg = error.message;
14772
- if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID") || errorMsg.includes("not found")) {
14773
- return new Error(
14774
- `UID "${uid}" is stale or invalid.
14775
-
14776
- The page may have changed since the snapshot was taken.
14777
- Please call take_snapshot to get fresh UIDs and try again.`
14778
- );
14779
- }
14780
- return error;
14781
- }
14782
14731
  async function handleClickByUid(args2) {
14783
14732
  try {
14784
14733
  const { uid, dblClick } = args2;
@@ -14789,9 +14738,7 @@ async function handleClickByUid(args2) {
14789
14738
  const firefox3 = await getFirefox2();
14790
14739
  try {
14791
14740
  await firefox3.clickByUid(uid, dblClick);
14792
- return successResponse(
14793
- `\u2705 ${dblClick ? "Double-clicked" : "Clicked"} element with UID "${uid}"`
14794
- );
14741
+ return successResponse(`\u2705 ${dblClick ? "dblclick" : "click"} ${uid}`);
14795
14742
  } catch (error) {
14796
14743
  throw handleUidError(error, uid);
14797
14744
  }
@@ -14809,7 +14756,7 @@ async function handleHoverByUid(args2) {
14809
14756
  const firefox3 = await getFirefox2();
14810
14757
  try {
14811
14758
  await firefox3.hoverByUid(uid);
14812
- return successResponse(`\u2705 Hovered over element with UID "${uid}"`);
14759
+ return successResponse(`\u2705 hover ${uid}`);
14813
14760
  } catch (error) {
14814
14761
  throw handleUidError(error, uid);
14815
14762
  }
@@ -14830,10 +14777,7 @@ async function handleFillByUid(args2) {
14830
14777
  const firefox3 = await getFirefox2();
14831
14778
  try {
14832
14779
  await firefox3.fillByUid(uid, value);
14833
- return successResponse(
14834
- `\u2705 Filled element with UID "${uid}"
14835
- Value: ${value.substring(0, 50)}${value.length > 50 ? "..." : ""}`
14836
- );
14780
+ return successResponse(`\u2705 fill ${uid}`);
14837
14781
  } catch (error) {
14838
14782
  throw handleUidError(error, uid);
14839
14783
  }
@@ -14854,16 +14798,11 @@ async function handleDragByUidToUid(args2) {
14854
14798
  const firefox3 = await getFirefox2();
14855
14799
  try {
14856
14800
  await firefox3.dragByUidToUid(fromUid, toUid);
14857
- return successResponse(`\u2705 Dragged element "${fromUid}" to "${toUid}"`);
14801
+ return successResponse(`\u2705 drag ${fromUid}\u2192${toUid}`);
14858
14802
  } catch (error) {
14859
14803
  const errorMsg = error.message;
14860
14804
  if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID")) {
14861
- throw new Error(
14862
- `One or both UIDs (from: "${fromUid}", to: "${toUid}") are stale or invalid.
14863
-
14864
- The page may have changed since the snapshot was taken.
14865
- Please call take_snapshot to get fresh UIDs and try again.`
14866
- );
14805
+ throw new Error(`UIDs stale/invalid. Call take_snapshot first.`);
14867
14806
  }
14868
14807
  throw error;
14869
14808
  }
@@ -14889,21 +14828,11 @@ async function handleFillFormByUid(args2) {
14889
14828
  const firefox3 = await getFirefox2();
14890
14829
  try {
14891
14830
  await firefox3.fillFormByUid(elements);
14892
- return successResponse(
14893
- `\u2705 Filled ${elements.length} form field(s):
14894
- ` + elements.map(
14895
- (el) => ` - ${el.uid}: ${el.value.substring(0, 30)}${el.value.length > 30 ? "..." : ""}`
14896
- ).join("\n")
14897
- );
14831
+ return successResponse(`\u2705 filled ${elements.length} fields`);
14898
14832
  } catch (error) {
14899
14833
  const errorMsg = error.message;
14900
14834
  if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID")) {
14901
- throw new Error(
14902
- `One or more UIDs are stale or invalid.
14903
-
14904
- The page may have changed since the snapshot was taken.
14905
- Please call take_snapshot to get fresh UIDs and try again.`
14906
- );
14835
+ throw new Error(`UIDs stale/invalid. Call take_snapshot first.`);
14907
14836
  }
14908
14837
  throw error;
14909
14838
  }
@@ -14924,26 +14853,17 @@ async function handleUploadFileByUid(args2) {
14924
14853
  const firefox3 = await getFirefox2();
14925
14854
  try {
14926
14855
  await firefox3.uploadFileByUid(uid, filePath);
14927
- return successResponse(`\u2705 Uploaded file to element with UID "${uid}"
14928
- File: ${filePath}`);
14856
+ return successResponse(`\u2705 upload ${uid}`);
14929
14857
  } catch (error) {
14930
14858
  const errorMsg = error.message;
14931
14859
  if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID")) {
14932
14860
  throw handleUidError(error, uid);
14933
14861
  }
14934
14862
  if (errorMsg.includes("not a file input") || errorMsg.includes('type="file"')) {
14935
- throw new Error(
14936
- `Element with UID "${uid}" is not an <input type="file"> element.
14937
-
14938
- Please ensure the UID points to a file input element.`
14939
- );
14863
+ throw new Error(`${uid} is not a file input`);
14940
14864
  }
14941
14865
  if (errorMsg.includes("hidden") || errorMsg.includes("not visible")) {
14942
- throw new Error(
14943
- `File input element with UID "${uid}" is hidden or not interactable.
14944
-
14945
- Some file inputs are hidden and cannot be directly interacted with.`
14946
- );
14866
+ throw new Error(`${uid} is hidden/not interactable`);
14947
14867
  }
14948
14868
  throw error;
14949
14869
  }
@@ -14956,19 +14876,20 @@ var init_input = __esm({
14956
14876
  "src/tools/input.ts"() {
14957
14877
  "use strict";
14958
14878
  init_response_helpers();
14879
+ init_uid_helpers();
14959
14880
  clickByUidTool = {
14960
14881
  name: "click_by_uid",
14961
- 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.",
14962
14883
  inputSchema: {
14963
14884
  type: "object",
14964
14885
  properties: {
14965
14886
  uid: {
14966
14887
  type: "string",
14967
- description: "The UID of the element to click"
14888
+ description: "Element UID from snapshot"
14968
14889
  },
14969
14890
  dblClick: {
14970
14891
  type: "boolean",
14971
- description: "If true, performs a double-click (default: false)"
14892
+ description: "Double-click (default: false)"
14972
14893
  }
14973
14894
  },
14974
14895
  required: ["uid"]
@@ -14976,13 +14897,13 @@ var init_input = __esm({
14976
14897
  };
14977
14898
  hoverByUidTool = {
14978
14899
  name: "hover_by_uid",
14979
- 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.",
14980
14901
  inputSchema: {
14981
14902
  type: "object",
14982
14903
  properties: {
14983
14904
  uid: {
14984
14905
  type: "string",
14985
- description: "The UID of the element to hover over"
14906
+ description: "Element UID from snapshot"
14986
14907
  }
14987
14908
  },
14988
14909
  required: ["uid"]
@@ -14990,17 +14911,17 @@ var init_input = __esm({
14990
14911
  };
14991
14912
  fillByUidTool = {
14992
14913
  name: "fill_by_uid",
14993
- 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.",
14994
14915
  inputSchema: {
14995
14916
  type: "object",
14996
14917
  properties: {
14997
14918
  uid: {
14998
14919
  type: "string",
14999
- description: "The UID of the input element"
14920
+ description: "Input element UID from snapshot"
15000
14921
  },
15001
14922
  value: {
15002
14923
  type: "string",
15003
- description: "The text value to fill into the input"
14924
+ description: "Text to fill"
15004
14925
  }
15005
14926
  },
15006
14927
  required: ["uid", "value"]
@@ -15008,17 +14929,17 @@ var init_input = __esm({
15008
14929
  };
15009
14930
  dragByUidToUidTool = {
15010
14931
  name: "drag_by_uid_to_uid",
15011
- 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).",
15012
14933
  inputSchema: {
15013
14934
  type: "object",
15014
14935
  properties: {
15015
14936
  fromUid: {
15016
14937
  type: "string",
15017
- description: "The UID of the element to drag"
14938
+ description: "Source element UID"
15018
14939
  },
15019
14940
  toUid: {
15020
14941
  type: "string",
15021
- description: "The UID of the target element to drop onto"
14942
+ description: "Target element UID"
15022
14943
  }
15023
14944
  },
15024
14945
  required: ["fromUid", "toUid"]
@@ -15026,23 +14947,23 @@ var init_input = __esm({
15026
14947
  };
15027
14948
  fillFormByUidTool = {
15028
14949
  name: "fill_form_by_uid",
15029
- description: "Fill multiple form fields in one call using an array of {uid, value} pairs.",
14950
+ description: "Fill multiple form fields at once.",
15030
14951
  inputSchema: {
15031
14952
  type: "object",
15032
14953
  properties: {
15033
14954
  elements: {
15034
14955
  type: "array",
15035
- description: "Array of form field UIDs with their values",
14956
+ description: "Array of {uid, value} pairs",
15036
14957
  items: {
15037
14958
  type: "object",
15038
14959
  properties: {
15039
14960
  uid: {
15040
14961
  type: "string",
15041
- description: "The UID of the form field"
14962
+ description: "Field UID"
15042
14963
  },
15043
14964
  value: {
15044
14965
  type: "string",
15045
- description: "The value to fill"
14966
+ description: "Field value"
15046
14967
  }
15047
14968
  },
15048
14969
  required: ["uid", "value"]
@@ -15054,17 +14975,17 @@ var init_input = __esm({
15054
14975
  };
15055
14976
  uploadFileByUidTool = {
15056
14977
  name: "upload_file_by_uid",
15057
- 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.",
15058
14979
  inputSchema: {
15059
14980
  type: "object",
15060
14981
  properties: {
15061
14982
  uid: {
15062
14983
  type: "string",
15063
- description: "The UID of the file input element"
14984
+ description: "File input UID from snapshot"
15064
14985
  },
15065
14986
  filePath: {
15066
14987
  type: "string",
15067
- description: "Local filesystem path to the file to upload"
14988
+ description: "Local file path"
15068
14989
  }
15069
14990
  },
15070
14991
  required: ["uid", "filePath"]
@@ -15074,36 +14995,16 @@ var init_input = __esm({
15074
14995
  });
15075
14996
 
15076
14997
  // src/tools/screenshot.ts
15077
- function buildScreenshotResponse(base64Png, context) {
14998
+ function buildScreenshotResponse(base64Png, label) {
15078
14999
  const sizeKB = Math.round(base64Png.length / 1024);
15079
- const estimatedTokens = estimateTokens(base64Png);
15080
15000
  if (base64Png.length > TOKEN_LIMITS.MAX_SCREENSHOT_CHARS) {
15081
15001
  const truncatedData = base64Png.slice(0, TOKEN_LIMITS.MAX_SCREENSHOT_CHARS);
15082
- return successResponse(
15083
- `\u{1F4F8} ${context} (${sizeKB}KB)
15084
-
15085
- \u26A0\uFE0F Screenshot truncated (~${Math.round(estimatedTokens / 1e3)}k tokens exceeds limit)
15086
- Only first ${Math.round(TOKEN_LIMITS.MAX_SCREENSHOT_CHARS / 1024)}KB shown.
15087
- TIP: For full screenshots, use a dedicated screenshot tool or save to file.
15088
-
15089
- Base64 PNG data (truncated):
15090
- ${truncatedData}
15091
-
15092
- [...truncated]`
15093
- );
15094
- }
15095
- let warning = "";
15096
- if (base64Png.length > TOKEN_LIMITS.WARNING_THRESHOLD_CHARS) {
15097
- warning = `\u26A0\uFE0F Large screenshot (~${Math.round(estimatedTokens / 1e3)}k tokens) - may fill context quickly
15098
-
15099
- `;
15002
+ return successResponse(`\u{1F4F8} ${label} (${sizeKB}KB) [truncated]
15003
+ ${truncatedData}`);
15100
15004
  }
15101
- return successResponse(
15102
- `\u{1F4F8} ${context} (${sizeKB}KB)
15103
-
15104
- ` + warning + `Base64 PNG data:
15105
- ${base64Png}`
15106
- );
15005
+ const warn = base64Png.length > TOKEN_LIMITS.WARNING_THRESHOLD_CHARS ? " [large]" : "";
15006
+ return successResponse(`\u{1F4F8} ${label} (${sizeKB}KB)${warn}
15007
+ ${base64Png}`);
15107
15008
  }
15108
15009
  async function handleScreenshotPage(_args) {
15109
15010
  try {
@@ -15111,44 +15012,29 @@ async function handleScreenshotPage(_args) {
15111
15012
  const firefox3 = await getFirefox2();
15112
15013
  const base64Png = await firefox3.takeScreenshotPage();
15113
15014
  if (!base64Png || typeof base64Png !== "string") {
15114
- throw new Error("Failed to capture screenshot: invalid data returned");
15015
+ throw new Error("Invalid screenshot data");
15115
15016
  }
15116
- return buildScreenshotResponse(base64Png, "Page screenshot captured");
15017
+ return buildScreenshotResponse(base64Png, "page");
15117
15018
  } catch (error) {
15118
- return errorResponse(
15119
- new Error(
15120
- `Failed to capture page screenshot: ${error.message}
15121
-
15122
- The page may not be fully loaded or accessible.`
15123
- )
15124
- );
15019
+ return errorResponse(error);
15125
15020
  }
15126
15021
  }
15127
15022
  async function handleScreenshotByUid(args2) {
15128
15023
  try {
15129
15024
  const { uid } = args2;
15130
15025
  if (!uid || typeof uid !== "string") {
15131
- throw new Error("uid parameter is required and must be a string");
15026
+ throw new Error("uid required");
15132
15027
  }
15133
15028
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
15134
15029
  const firefox3 = await getFirefox2();
15135
15030
  try {
15136
15031
  const base64Png = await firefox3.takeScreenshotByUid(uid);
15137
15032
  if (!base64Png || typeof base64Png !== "string") {
15138
- throw new Error("Failed to capture screenshot: invalid data returned");
15033
+ throw new Error("Invalid screenshot data");
15139
15034
  }
15140
- return buildScreenshotResponse(base64Png, `Element screenshot captured for UID "${uid}"`);
15035
+ return buildScreenshotResponse(base64Png, uid);
15141
15036
  } catch (error) {
15142
- const errorMsg = error.message;
15143
- if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID") || errorMsg.includes("not found")) {
15144
- throw new Error(
15145
- `UID "${uid}" is stale or invalid.
15146
-
15147
- The page may have changed since the snapshot was taken.
15148
- Please call take_snapshot to get fresh UIDs and try again.`
15149
- );
15150
- }
15151
- throw error;
15037
+ throw handleUidError(error, uid);
15152
15038
  }
15153
15039
  } catch (error) {
15154
15040
  return errorResponse(error);
@@ -15159,9 +15045,10 @@ var init_screenshot = __esm({
15159
15045
  "src/tools/screenshot.ts"() {
15160
15046
  "use strict";
15161
15047
  init_response_helpers();
15048
+ init_uid_helpers();
15162
15049
  screenshotPageTool = {
15163
15050
  name: "screenshot_page",
15164
- 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.",
15165
15052
  inputSchema: {
15166
15053
  type: "object",
15167
15054
  properties: {}
@@ -15169,13 +15056,13 @@ var init_screenshot = __esm({
15169
15056
  };
15170
15057
  screenshotByUidTool = {
15171
15058
  name: "screenshot_by_uid",
15172
- 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.",
15173
15060
  inputSchema: {
15174
15061
  type: "object",
15175
15062
  properties: {
15176
15063
  uid: {
15177
15064
  type: "string",
15178
- description: "The UID of the element to screenshot"
15065
+ description: "Element UID from snapshot"
15179
15066
  }
15180
15067
  },
15181
15068
  required: ["uid"]
@@ -15192,17 +15079,11 @@ async function handleAcceptDialog(args2) {
15192
15079
  const firefox3 = await getFirefox2();
15193
15080
  try {
15194
15081
  await firefox3.acceptDialog(promptText);
15195
- let message = "\u2705 Dialog accepted";
15196
- if (promptText) {
15197
- message += ` with text: "${promptText}"`;
15198
- }
15199
- return successResponse(message);
15082
+ return successResponse(promptText ? `\u2705 Accepted: "${promptText}"` : "\u2705 Accepted");
15200
15083
  } catch (error) {
15201
15084
  const errorMsg = error.message;
15202
15085
  if (errorMsg.includes("no such alert") || errorMsg.includes("No dialog")) {
15203
- throw new Error(
15204
- "No active dialog found.\n\nDialogs must be accepted shortly after they appear. Make sure a dialog is currently visible on the page."
15205
- );
15086
+ throw new Error("No active dialog");
15206
15087
  }
15207
15088
  throw error;
15208
15089
  }
@@ -15216,13 +15097,11 @@ async function handleDismissDialog(_args) {
15216
15097
  const firefox3 = await getFirefox2();
15217
15098
  try {
15218
15099
  await firefox3.dismissDialog();
15219
- return successResponse("\u2705 Dialog dismissed/cancelled");
15100
+ return successResponse("\u2705 Dismissed");
15220
15101
  } catch (error) {
15221
15102
  const errorMsg = error.message;
15222
15103
  if (errorMsg.includes("no such alert") || errorMsg.includes("No dialog")) {
15223
- throw new Error(
15224
- "No active dialog found.\n\nDialogs must be dismissed shortly after they appear. Make sure a dialog is currently visible on the page."
15225
- );
15104
+ throw new Error("No active dialog");
15226
15105
  }
15227
15106
  throw error;
15228
15107
  }
@@ -15243,19 +15122,9 @@ async function handleNavigateHistory(args2) {
15243
15122
  } else {
15244
15123
  await firefox3.navigateForward();
15245
15124
  }
15246
- return successResponse(
15247
- `\u2705 Navigated ${direction} in history
15248
-
15249
- \u26A0\uFE0F UIDs from previous snapshots are now stale. Call take_snapshot before using UID-based actions.`
15250
- );
15125
+ return successResponse(`\u2705 ${direction}`);
15251
15126
  } catch (error) {
15252
- return errorResponse(
15253
- new Error(
15254
- `Failed to navigate ${args2.direction || "in history"}: ${error.message}
15255
-
15256
- The page may not have history in this direction available.`
15257
- )
15258
- );
15127
+ return errorResponse(error);
15259
15128
  }
15260
15129
  }
15261
15130
  async function handleSetViewportSize(args2) {
@@ -15270,19 +15139,9 @@ async function handleSetViewportSize(args2) {
15270
15139
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
15271
15140
  const firefox3 = await getFirefox2();
15272
15141
  await firefox3.setViewportSize(width, height);
15273
- return successResponse(
15274
- `\u2705 Viewport size set to ${width}x${height} pixels
15275
-
15276
- NOTE: Actual viewport may differ slightly in some browser modes (e.g., headless).`
15277
- );
15142
+ return successResponse(`\u2705 ${width}x${height}`);
15278
15143
  } catch (error) {
15279
- return errorResponse(
15280
- new Error(
15281
- `Failed to set viewport size: ${error.message}
15282
-
15283
- Some browser configurations may not support precise viewport sizing.`
15284
- )
15285
- );
15144
+ return errorResponse(error);
15286
15145
  }
15287
15146
  }
15288
15147
  var acceptDialogTool, dismissDialogTool, navigateHistoryTool, setViewportSizeTool;
@@ -15292,20 +15151,20 @@ var init_utilities = __esm({
15292
15151
  init_response_helpers();
15293
15152
  acceptDialogTool = {
15294
15153
  name: "accept_dialog",
15295
- 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.",
15296
15155
  inputSchema: {
15297
15156
  type: "object",
15298
15157
  properties: {
15299
15158
  promptText: {
15300
15159
  type: "string",
15301
- description: "Text to enter in a prompt dialog (optional, only for prompt dialogs)"
15160
+ description: "Text for prompt dialogs"
15302
15161
  }
15303
15162
  }
15304
15163
  }
15305
15164
  };
15306
15165
  dismissDialogTool = {
15307
15166
  name: "dismiss_dialog",
15308
- description: "Dismiss the active browser dialog (alert/confirm/prompt). Returns an error if no dialog is open.",
15167
+ description: "Dismiss browser dialog.",
15309
15168
  inputSchema: {
15310
15169
  type: "object",
15311
15170
  properties: {}
@@ -15313,14 +15172,14 @@ var init_utilities = __esm({
15313
15172
  };
15314
15173
  navigateHistoryTool = {
15315
15174
  name: "navigate_history",
15316
- 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.",
15317
15176
  inputSchema: {
15318
15177
  type: "object",
15319
15178
  properties: {
15320
15179
  direction: {
15321
15180
  type: "string",
15322
15181
  enum: ["back", "forward"],
15323
- description: "Direction to navigate in history"
15182
+ description: "back or forward"
15324
15183
  }
15325
15184
  },
15326
15185
  required: ["direction"]
@@ -15328,17 +15187,17 @@ var init_utilities = __esm({
15328
15187
  };
15329
15188
  setViewportSizeTool = {
15330
15189
  name: "set_viewport_size",
15331
- 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.",
15332
15191
  inputSchema: {
15333
15192
  type: "object",
15334
15193
  properties: {
15335
15194
  width: {
15336
15195
  type: "number",
15337
- description: "Viewport width in pixels"
15196
+ description: "Width in pixels"
15338
15197
  },
15339
15198
  height: {
15340
15199
  type: "number",
15341
- description: "Viewport height in pixels"
15200
+ description: "Height in pixels"
15342
15201
  }
15343
15202
  },
15344
15203
  required: ["width", "height"]
@@ -15824,7 +15683,8 @@ async function getFirefox() {
15824
15683
  profilePath: args.profilePath ?? void 0,
15825
15684
  viewport: args.viewport ?? void 0,
15826
15685
  args: args.firefoxArg ?? void 0,
15827
- startUrl: args.startUrl ?? void 0
15686
+ startUrl: args.startUrl ?? void 0,
15687
+ acceptInsecureCerts: args.acceptInsecureCerts
15828
15688
  };
15829
15689
  firefox2 = new FirefoxClient(options);
15830
15690
  await firefox2.connect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firefox-devtools-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.2",
4
4
  "description": "Model Context Protocol (MCP) server for Firefox DevTools automation",
5
5
  "author": "freema",
6
6
  "license": "MIT",
@@ -28,6 +28,7 @@
28
28
  "inspector:dev": "NODE_ENV=development npx @modelcontextprotocol/inspector npx tsx src/index.ts",
29
29
  "test": "vitest",
30
30
  "test:run": "vitest run",
31
+ "test:unit": "vitest run --exclude 'tests/integration/**'",
31
32
  "test:coverage": "vitest run --coverage",
32
33
  "test:watch": "vitest watch",
33
34
  "test:ui": "vitest --ui",