brave-real-browser-mcp-server 2.24.4 → 2.24.5

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.
@@ -586,22 +586,78 @@ export async function handleDeepAnalysis(page, args) {
586
586
  }
587
587
  /**
588
588
  * Record full network traffic - Uses response events to avoid crashes
589
+ * ULTRA POWERFUL: API detection, media URLs, smart categorization
589
590
  */
590
591
  export async function handleNetworkRecorder(page, args) {
591
592
  const requests = [];
592
593
  const duration = args.duration || 10000;
593
594
  let totalSize = 0;
595
+ const categories = {};
596
+ const apis = [];
597
+ const mediaUrls = [];
598
+ const seen = new Set();
599
+ // ============================================================
600
+ // SMART CATEGORIZATION HELPER
601
+ // ============================================================
602
+ const categorizeUrl = (url, resourceType) => {
603
+ const urlLower = url.toLowerCase();
604
+ // API endpoints
605
+ if (/\/api\/|\/v\d+\/|\.json(\?|$)|graphql/i.test(url))
606
+ return 'api';
607
+ // Media
608
+ if (/\.(mp4|webm|m3u8|ts|mp3|flac|ogg)/i.test(url))
609
+ return 'media';
610
+ if (resourceType === 'media' || resourceType === 'video' || resourceType === 'audio')
611
+ return 'media';
612
+ // Images
613
+ if (/\.(jpg|jpeg|png|gif|webp|svg|ico)/i.test(url) || resourceType === 'image')
614
+ return 'image';
615
+ // Scripts
616
+ if (/\.js(\?|$)/i.test(url) || resourceType === 'script')
617
+ return 'script';
618
+ // Styles
619
+ if (/\.css(\?|$)/i.test(url) || resourceType === 'stylesheet')
620
+ return 'style';
621
+ // Fonts
622
+ if (/\.(woff2?|ttf|eot|otf)/i.test(url) || resourceType === 'font')
623
+ return 'font';
624
+ // XHR/Fetch
625
+ if (resourceType === 'xhr' || resourceType === 'fetch')
626
+ return 'xhr';
627
+ // Documents
628
+ if (resourceType === 'document')
629
+ return 'document';
630
+ return 'other';
631
+ };
594
632
  // Response handler - safer than request interception
595
633
  const responseHandler = (response) => {
596
634
  try {
597
635
  const url = response.url();
636
+ // Dedup
637
+ if (seen.has(url))
638
+ return;
639
+ seen.add(url);
598
640
  if (args.filterUrl && !url.includes(args.filterUrl)) {
599
641
  return;
600
642
  }
643
+ const resourceType = response.request()?.resourceType?.() || 'unknown';
644
+ const method = response.request()?.method?.() || 'GET';
645
+ const category = categorizeUrl(url, resourceType);
646
+ categories[category] = (categories[category] || 0) + 1;
647
+ // Collect API endpoints
648
+ if (category === 'api' || resourceType === 'xhr' || resourceType === 'fetch') {
649
+ apis.push({ url, method, type: resourceType });
650
+ }
651
+ // Collect media URLs
652
+ if (category === 'media' || /\.(mp4|webm|m3u8|ts|mp3)/i.test(url)) {
653
+ mediaUrls.push(url);
654
+ }
601
655
  const entry = {
602
656
  url,
603
657
  status: response.status(),
604
- resourceType: response.request()?.resourceType?.() || 'unknown',
658
+ resourceType,
659
+ category,
660
+ method,
605
661
  timestamp: Date.now(),
606
662
  };
607
663
  if (args.includeHeaders) {
@@ -612,7 +668,6 @@ export async function handleNetworkRecorder(page, args) {
612
668
  entry.headers = {};
613
669
  }
614
670
  }
615
- // Note: Response body requires async handling, skip for stability
616
671
  requests.push(entry);
617
672
  // Track size from headers
618
673
  try {
@@ -647,6 +702,10 @@ export async function handleNetworkRecorder(page, args) {
647
702
  requests: requests.slice(0, 500),
648
703
  count: requests.length,
649
704
  totalSize,
705
+ categories,
706
+ apis: apis.length > 0 ? apis : undefined,
707
+ mediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
708
+ message: `📡 Recorded ${requests.length} requests (${Math.round(totalSize / 1024)}KB) | APIs: ${apis.length} | Media: ${mediaUrls.length}`
650
709
  };
651
710
  }
652
711
  /**
@@ -776,6 +835,7 @@ export async function handleAdProtectionDetector(page, args) {
776
835
  }
777
836
  /**
778
837
  * Wait for dynamic AJAX loading
838
+ * ULTRA POWERFUL: Infinite scroll, lazy load, mutation observer
779
839
  */
780
840
  export async function handleAjaxContentWaiter(page, args) {
781
841
  const timeout = args.timeout || 30000;
@@ -783,6 +843,79 @@ export async function handleAjaxContentWaiter(page, args) {
783
843
  const startTime = Date.now();
784
844
  let content;
785
845
  let loaded = false;
846
+ let newElementsCount = 0;
847
+ let scrollDepth = 0;
848
+ // ============================================================
849
+ // 1. MUTATION OBSERVER: Track DOM changes in real-time
850
+ // ============================================================
851
+ const setupMutationObserver = async () => {
852
+ return await page.evaluate(() => {
853
+ return new Promise((resolve) => {
854
+ let added = 0;
855
+ let modified = 0;
856
+ const observer = new MutationObserver((mutations) => {
857
+ mutations.forEach(m => {
858
+ added += m.addedNodes.length;
859
+ if (m.type === 'attributes' || m.type === 'characterData')
860
+ modified++;
861
+ });
862
+ });
863
+ observer.observe(document.body, {
864
+ childList: true,
865
+ subtree: true,
866
+ attributes: true,
867
+ characterData: true
868
+ });
869
+ // Return after 2 seconds of observation
870
+ setTimeout(() => {
871
+ observer.disconnect();
872
+ resolve({ added, modified });
873
+ }, 2000);
874
+ });
875
+ });
876
+ };
877
+ // ============================================================
878
+ // 2. INFINITE SCROLL DETECTION
879
+ // ============================================================
880
+ const handleInfiniteScroll = async () => {
881
+ const initialHeight = await page.evaluate(() => document.body.scrollHeight);
882
+ const initialCount = await page.evaluate(() => document.querySelectorAll('*').length);
883
+ // Scroll to bottom
884
+ await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
885
+ await new Promise(r => setTimeout(r, 1000));
886
+ // Check if new content loaded
887
+ const newHeight = await page.evaluate(() => document.body.scrollHeight);
888
+ const newCount = await page.evaluate(() => document.querySelectorAll('*').length);
889
+ return {
890
+ scrolled: newHeight > initialHeight,
891
+ newElements: newCount - initialCount,
892
+ scrollDepth: newHeight
893
+ };
894
+ };
895
+ // ============================================================
896
+ // 3. LAZY LOAD DETECTION
897
+ // ============================================================
898
+ const detectLazyLoad = async () => {
899
+ return await page.evaluate(() => {
900
+ const lazyElements = [];
901
+ // Check for common lazy load patterns
902
+ document.querySelectorAll('[data-src], [data-lazy], [loading="lazy"], .lazy, .lazyload').forEach(el => {
903
+ const dataSrc = el.getAttribute('data-src') || el.getAttribute('data-lazy');
904
+ if (dataSrc)
905
+ lazyElements.push(dataSrc);
906
+ });
907
+ // Intersection Observer based lazy images
908
+ document.querySelectorAll('img[data-src], img.lazy').forEach(img => {
909
+ const dataSrc = img.dataset.src;
910
+ if (dataSrc)
911
+ lazyElements.push(dataSrc);
912
+ });
913
+ return lazyElements;
914
+ });
915
+ };
916
+ // ============================================================
917
+ // 4. MAIN WAITING LOGIC
918
+ // ============================================================
786
919
  while (Date.now() - startTime < timeout) {
787
920
  if (args.selector) {
788
921
  const element = await page.$(args.selector);
@@ -795,17 +928,35 @@ export async function handleAjaxContentWaiter(page, args) {
795
928
  }
796
929
  }
797
930
  else {
798
- // Wait for network to be idle
799
- await page.waitForNetworkIdle({ timeout: pollInterval }).catch(() => { });
800
- loaded = true;
801
- break;
931
+ // Smart waiting: Check for ongoing activity
932
+ const mutationResult = await setupMutationObserver();
933
+ newElementsCount = mutationResult.added;
934
+ if (mutationResult.added === 0 && mutationResult.modified === 0) {
935
+ // No DOM changes, content likely loaded
936
+ loaded = true;
937
+ break;
938
+ }
939
+ }
940
+ // Try infinite scroll to load more content
941
+ const scrollResult = await handleInfiniteScroll();
942
+ if (scrollResult.scrolled) {
943
+ scrollDepth = scrollResult.scrollDepth;
944
+ newElementsCount += scrollResult.newElements;
802
945
  }
803
946
  await new Promise((r) => setTimeout(r, pollInterval));
804
947
  }
948
+ // Detect any lazy-loaded content
949
+ const lazyElements = await detectLazyLoad();
805
950
  return {
806
951
  loaded,
807
952
  waitTime: Date.now() - startTime,
808
953
  content,
954
+ newElementsCount,
955
+ scrollDepth,
956
+ lazyElements: lazyElements.length > 0 ? lazyElements : undefined,
957
+ message: loaded
958
+ ? `✅ Content loaded in ${Date.now() - startTime}ms (${newElementsCount} new elements, scroll: ${scrollDepth}px)`
959
+ : `⏱️ Timeout after ${timeout}ms`
809
960
  };
810
961
  }
811
962
  /**
@@ -1002,20 +1153,124 @@ export async function handleVideoRecording(page, args, recorderState) {
1002
1153
  }
1003
1154
  /**
1004
1155
  * Harvest all links from page
1156
+ * ULTRA POWERFUL: Pagination detection, smart categorization, file types
1005
1157
  */
1006
1158
  export async function handleLinkHarvester(page, args) {
1007
1159
  const currentUrl = new URL(page.url());
1008
- const allLinks = await page.evaluate((filter) => {
1009
- return Array.from(document.querySelectorAll('a[href]')).map((a) => ({
1010
- url: a.href,
1011
- text: a.textContent?.trim()?.substring(0, 100) || '',
1012
- }));
1013
- }, args.filter);
1160
+ // ============================================================
1161
+ // 1. EXTRACT ALL LINKS WITH SMART CATEGORIZATION
1162
+ // ============================================================
1163
+ const allLinks = await page.evaluate(() => {
1164
+ const links = [];
1165
+ document.querySelectorAll('a[href]').forEach((a) => {
1166
+ const anchor = a;
1167
+ links.push({
1168
+ url: anchor.href,
1169
+ text: a.textContent?.trim()?.substring(0, 100) || '',
1170
+ attrs: {
1171
+ rel: anchor.rel || '',
1172
+ target: anchor.target || '',
1173
+ class: anchor.className || '',
1174
+ id: anchor.id || '',
1175
+ download: anchor.download || '',
1176
+ }
1177
+ });
1178
+ });
1179
+ return links;
1180
+ });
1181
+ // ============================================================
1182
+ // 2. PAGINATION DETECTION
1183
+ // ============================================================
1184
+ const pagination = await page.evaluate(() => {
1185
+ let nextPage;
1186
+ let prevPage;
1187
+ let totalPages;
1188
+ // Common pagination selectors
1189
+ const nextSelectors = [
1190
+ 'a[rel="next"]', 'a.next', 'a.pagination-next',
1191
+ '[aria-label="Next"]', 'a:has-text("Next")', 'a:has-text(">")',
1192
+ '.pagination a:last-child', 'a.page-link:last-child'
1193
+ ];
1194
+ const prevSelectors = [
1195
+ 'a[rel="prev"]', 'a.prev', 'a.pagination-prev',
1196
+ '[aria-label="Previous"]', 'a:has-text("Prev")', 'a:has-text("<")'
1197
+ ];
1198
+ for (const sel of nextSelectors) {
1199
+ try {
1200
+ const el = document.querySelector(sel);
1201
+ if (el?.href) {
1202
+ nextPage = el.href;
1203
+ break;
1204
+ }
1205
+ }
1206
+ catch { /* invalid selector */ }
1207
+ }
1208
+ for (const sel of prevSelectors) {
1209
+ try {
1210
+ const el = document.querySelector(sel);
1211
+ if (el?.href) {
1212
+ prevPage = el.href;
1213
+ break;
1214
+ }
1215
+ }
1216
+ catch { /* invalid selector */ }
1217
+ }
1218
+ // Count page numbers
1219
+ const pageNumbers = Array.from(document.querySelectorAll('.pagination a, .page-numbers a, nav a'))
1220
+ .map(a => parseInt(a.textContent || '0', 10))
1221
+ .filter(n => !isNaN(n) && n > 0);
1222
+ if (pageNumbers.length > 0) {
1223
+ totalPages = Math.max(...pageNumbers);
1224
+ }
1225
+ return { nextPage, prevPage, totalPages };
1226
+ });
1227
+ // ============================================================
1228
+ // 3. SMART LINK CATEGORIZATION
1229
+ // ============================================================
1230
+ const categorizeLink = (url, text, attrs) => {
1231
+ const urlLower = url.toLowerCase();
1232
+ const textLower = text.toLowerCase();
1233
+ // File downloads
1234
+ if (/\.(pdf|doc|docx|xls|xlsx|zip|rar|7z|tar|gz)(\?.*)?$/i.test(url))
1235
+ return 'document';
1236
+ if (/\.(mp4|mkv|avi|mov|webm|flv)(\?.*)?$/i.test(url))
1237
+ return 'video';
1238
+ if (/\.(mp3|wav|flac|aac|ogg)(\?.*)?$/i.test(url))
1239
+ return 'audio';
1240
+ if (/\.(jpg|jpeg|png|gif|webp|svg|bmp)(\?.*)?$/i.test(url))
1241
+ return 'image';
1242
+ if (attrs.download)
1243
+ return 'download';
1244
+ // Navigation
1245
+ if (/\/(next|page|p)\/\d+|[?&]page=\d+/i.test(url))
1246
+ return 'pagination';
1247
+ if (textLower.includes('next') || textLower.includes('prev'))
1248
+ return 'pagination';
1249
+ // Social
1250
+ if (/facebook|twitter|instagram|linkedin|youtube|tiktok/i.test(url))
1251
+ return 'social';
1252
+ // Common patterns
1253
+ if (/login|signin|sign-in/i.test(url))
1254
+ return 'auth';
1255
+ if (/register|signup|sign-up/i.test(url))
1256
+ return 'auth';
1257
+ if (/search|query|q=/i.test(url))
1258
+ return 'search';
1259
+ if (/contact|about|faq|help/i.test(url))
1260
+ return 'info';
1261
+ return 'navigation';
1262
+ };
1014
1263
  const processedLinks = [];
1264
+ const categories = {};
1265
+ const seen = new Set();
1015
1266
  let internal = 0;
1016
1267
  let external = 0;
1017
1268
  for (const link of allLinks) {
1018
1269
  try {
1270
+ // Dedup by URL
1271
+ if (seen.has(link.url))
1272
+ continue;
1273
+ seen.add(link.url);
1019
1274
  const linkUrl = new URL(link.url);
1020
1275
  const isInternal = linkUrl.hostname === currentUrl.hostname;
1021
1276
  if (args.filter && !link.url.includes(args.filter) && !link.text.includes(args.filter)) {
@@ -1025,10 +1280,13 @@ export async function handleLinkHarvester(page, args) {
1025
1280
  continue;
1026
1281
  if (!isInternal && args.includeExternal === false)
1027
1282
  continue;
1283
+ const category = categorizeLink(link.url, link.text, link.attrs);
1284
+ categories[category] = (categories[category] || 0) + 1;
1028
1285
  processedLinks.push({
1029
1286
  url: link.url,
1030
1287
  text: link.text,
1031
1288
  type: isInternal ? 'internal' : 'external',
1289
+ category,
1032
1290
  });
1033
1291
  if (isInternal)
1034
1292
  internal++;
@@ -1045,6 +1303,10 @@ export async function handleLinkHarvester(page, args) {
1045
1303
  links: processedLinks,
1046
1304
  internal,
1047
1305
  external,
1306
+ pagination: (pagination.nextPage || pagination.prevPage || pagination.totalPages) ? pagination : undefined,
1307
+ categories,
1308
+ message: `🔗 Found ${processedLinks.length} links (${internal} internal, ${external} external)` +
1309
+ (pagination.nextPage ? ` | Next: ${pagination.nextPage}` : '')
1048
1310
  };
1049
1311
  }
1050
1312
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-browser-mcp-server",
3
- "version": "2.24.4",
3
+ "version": "2.24.5",
4
4
  "description": "🦁 MCP server for Brave Real Browser - NPM Workspaces Monorepo with anti-detection features, SSE streaming, and LSP compatibility",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/sdk": "latest",
52
52
  "@types/turndown": "latest",
53
- "brave-real-browser": "^2.5.4",
53
+ "brave-real-browser": "^2.5.5",
54
54
  "turndown": "latest",
55
55
  "vscode-languageserver": "^9.0.1",
56
56
  "vscode-languageserver-textdocument": "^1.0.12"