brave-real-browser-mcp-server 2.12.8 → 2.12.10

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.
@@ -275,32 +275,97 @@ export async function handleVideoPlayPushSource(args) {
275
275
  }
276
276
  };
277
277
  page.on('response', responseHandler);
278
- // Try to find and click play button
278
+ // Enhanced play button selectors
279
279
  const playSelectors = [
280
280
  'button[class*="play"]',
281
281
  '[class*="play-button"]',
282
+ '[class*="btn-play"]',
282
283
  '[aria-label*="Play"]',
284
+ '[aria-label*="play"]',
285
+ 'button[title*="Play"]',
286
+ 'button[title*="play"]',
283
287
  '.video-play',
284
288
  '#play-button',
289
+ '#playButton',
290
+ '.play-btn',
291
+ 'video', // Direct video element
292
+ // Icon-based play buttons
293
+ '[class*="fa-play"]',
294
+ '[class*="icon-play"]',
295
+ 'i[class*="play"]',
285
296
  ];
286
297
  let clicked = false;
298
+ let clickMethod = 'none';
299
+ // Try clicking play buttons
287
300
  for (const selector of playSelectors) {
288
301
  try {
289
- await page.click(selector, { timeout: 2000 });
290
- clicked = true;
291
- break;
302
+ if (selector === 'video') {
303
+ // Try to play video directly
304
+ const played = await page.evaluate(() => {
305
+ const videos = document.querySelectorAll('video');
306
+ let success = false;
307
+ videos.forEach((video) => {
308
+ try {
309
+ video.play();
310
+ success = true;
311
+ }
312
+ catch (e) { }
313
+ });
314
+ return success;
315
+ });
316
+ if (played) {
317
+ clicked = true;
318
+ clickMethod = 'video.play()';
319
+ break;
320
+ }
321
+ }
322
+ else {
323
+ const element = await page.$(selector);
324
+ if (element) {
325
+ await element.click();
326
+ clicked = true;
327
+ clickMethod = selector;
328
+ break;
329
+ }
330
+ }
292
331
  }
293
332
  catch (e) {
294
333
  // Try next selector
295
334
  }
296
335
  }
297
- // Wait for sources to load
298
- await sleep(3000);
336
+ // If no play button found, try clicking center of iframe
337
+ if (!clicked) {
338
+ try {
339
+ const iframeClicked = await page.evaluate(() => {
340
+ const iframe = document.querySelector('iframe');
341
+ if (iframe) {
342
+ const rect = iframe.getBoundingClientRect();
343
+ const event = new MouseEvent('click', {
344
+ view: window,
345
+ bubbles: true,
346
+ cancelable: true,
347
+ clientX: rect.left + rect.width / 2,
348
+ clientY: rect.top + rect.height / 2
349
+ });
350
+ iframe.dispatchEvent(event);
351
+ return true;
352
+ }
353
+ return false;
354
+ });
355
+ if (iframeClicked) {
356
+ clicked = true;
357
+ clickMethod = 'iframe-click';
358
+ }
359
+ }
360
+ catch (e) { }
361
+ }
362
+ // Wait for sources to load (longer wait for iframe-based)
363
+ await sleep(5000);
299
364
  page.off('response', responseHandler);
300
365
  return {
301
366
  content: [{
302
367
  type: 'text',
303
- text: `✅ Video sources captured\n\nPlay button clicked: ${clicked}\nSources found: ${videoSources.length}\n\n${JSON.stringify(videoSources, null, 2)}`,
368
+ text: `✅ Video sources captured\n\n📊 Status:\n • Interaction attempted: ${clicked ? 'Yes' : 'No'}\n • Method: ${clickMethod}\n • Sources found: ${videoSources.length}\n\n${videoSources.length > 0 ? JSON.stringify(videoSources, null, 2) : '💡 Tip: This site may use iframe-embedded videos. Try navigating to the iframe URL and using advanced_video_extraction.'}`,
304
369
  }],
305
370
  };
306
371
  }, 'Failed to capture video play sources');
@@ -316,46 +381,119 @@ export async function handleVideoPlayButtonClick(args) {
316
381
  });
317
382
  const page = getCurrentPage();
318
383
  const customSelector = args.selector;
319
- const playSelectors = customSelector ? [customSelector] : [
384
+ // Enhanced play button selectors
385
+ const defaultSelectors = [
320
386
  'button[class*="play"]',
321
387
  '[class*="play-button"]',
388
+ '[class*="btn-play"]',
322
389
  '[aria-label*="Play"]',
390
+ '[aria-label*="play"]',
323
391
  'button[title*="Play"]',
392
+ 'button[title*="play"]',
324
393
  '.video-play',
394
+ '.play-btn',
325
395
  '#play-button',
326
- 'video', // Direct video element
396
+ '#playButton',
397
+ // Icon-based
398
+ 'button i[class*="play"]',
399
+ 'button i[class*="fa-play"]',
400
+ '[class*="fa-play"]',
401
+ '[class*="icon-play"]',
402
+ // Video element
403
+ 'video',
327
404
  ];
405
+ const playSelectors = customSelector ? [customSelector] : defaultSelectors;
406
+ const results = {
407
+ attempted: [],
408
+ clicked: false,
409
+ method: 'none',
410
+ selector: null
411
+ };
328
412
  for (const selector of playSelectors) {
329
413
  try {
330
- const element = await page.$(selector);
331
- if (element) {
332
- if (selector === 'video') {
333
- // For video element, use play() method
334
- await page.evaluate(() => {
335
- const video = document.querySelector('video');
336
- if (video)
414
+ if (selector === 'video') {
415
+ // For video element, use play() method
416
+ const played = await page.evaluate(() => {
417
+ const videos = document.querySelectorAll('video');
418
+ let success = false;
419
+ videos.forEach((video) => {
420
+ try {
337
421
  video.play();
422
+ success = true;
423
+ }
424
+ catch (e) { }
338
425
  });
426
+ return success;
427
+ });
428
+ results.attempted.push({ selector, found: played });
429
+ if (played) {
430
+ results.clicked = true;
431
+ results.method = 'video.play()';
432
+ results.selector = selector;
433
+ return {
434
+ content: [{
435
+ type: 'text',
436
+ text: `✅ Play button clicked\n\n📊 Details:\n • Method: ${results.method}\n • Selector: ${selector}\n • Attempts: ${results.attempted.length}`,
437
+ }],
438
+ };
339
439
  }
340
- else {
440
+ }
441
+ else {
442
+ const element = await page.$(selector);
443
+ results.attempted.push({ selector, found: !!element });
444
+ if (element) {
341
445
  await element.click();
446
+ results.clicked = true;
447
+ results.method = 'element.click()';
448
+ results.selector = selector;
449
+ return {
450
+ content: [{
451
+ type: 'text',
452
+ text: `✅ Play button clicked\n\n📊 Details:\n • Method: ${results.method}\n • Selector: ${selector}\n • Attempts: ${results.attempted.length}`,
453
+ }],
454
+ };
342
455
  }
343
- return {
344
- content: [{
345
- type: 'text',
346
- text: `✅ Play button clicked: ${selector}`,
347
- }],
348
- };
349
456
  }
350
457
  }
351
458
  catch (e) {
352
- // Try next selector
459
+ results.attempted.push({ selector, error: String(e) });
353
460
  }
354
461
  }
462
+ // Fallback: Try clicking iframe center
463
+ try {
464
+ const iframeInfo = await page.evaluate(() => {
465
+ const iframe = document.querySelector('iframe');
466
+ if (iframe) {
467
+ const rect = iframe.getBoundingClientRect();
468
+ return {
469
+ found: true,
470
+ x: rect.left + rect.width / 2,
471
+ y: rect.top + rect.height / 2,
472
+ src: iframe.src
473
+ };
474
+ }
475
+ return { found: false };
476
+ });
477
+ if (iframeInfo.found) {
478
+ await page.mouse.click(iframeInfo.x, iframeInfo.y);
479
+ results.clicked = true;
480
+ results.method = 'iframe-click';
481
+ results.selector = 'iframe (center)';
482
+ return {
483
+ content: [{
484
+ type: 'text',
485
+ text: `✅ Play action attempted\n\n📊 Details:\n • Method: iframe center click\n • Iframe src: ${iframeInfo.src}\n • Position: (${Math.round(iframeInfo.x)}, ${Math.round(iframeInfo.y)})\n\n💡 Tip: For iframe-based videos, navigate to iframe URL first`,
486
+ }],
487
+ };
488
+ }
489
+ }
490
+ catch (e) {
491
+ results.attempted.push({ selector: 'iframe-fallback', error: String(e) });
492
+ }
355
493
  return {
356
494
  content: [{
357
495
  type: 'text',
358
- text: `❌ No play button found`,
496
+ text: `⚠️ No direct play button found\n\n📊 Attempts: ${results.attempted.length}\n💡 Suggestions:\n • This site uses iframe-embedded videos\n • Use iframe_extractor to find video iframe\n • Navigate to iframe URL\n • Then use advanced_video_extraction\n\nAttempted selectors:\n${results.attempted.map((a) => ` • ${a.selector}: ${a.found ? '✓ found' : '✗ not found'}`).join('\n')}`,
359
497
  }],
360
498
  };
361
499
  }, 'Failed to click play button');
@@ -449,13 +587,17 @@ export async function handleNetworkRecordingFinder(args) {
449
587
  const isStreamAsset = /\.m3u8(\?|$)|\.mpd(\?|$)|\.ts(\?|$)|\.vtt(\?|$)|\.mp4(\?|$)|\.webm(\?|$)/i.test(urlLower) ||
450
588
  contentType.includes('application/vnd.apple.mpegurl') ||
451
589
  contentType.includes('application/x-mpegurl');
452
- if (filterType === 'video' && (contentType.includes('video') || resourceType === 'media' || isStreamAsset)) {
590
+ // Video API detection (like /api/v1/video, /stream, etc.)
591
+ const isVideoAPI = urlLower.includes('/video') ||
592
+ urlLower.includes('/stream') ||
593
+ (contentType.includes('application/octet-stream') && urlLower.includes('video'));
594
+ if (filterType === 'video' && (contentType.includes('video') || resourceType === 'media' || isStreamAsset || isVideoAPI)) {
453
595
  shouldRecord = true;
454
596
  }
455
597
  else if (filterType === 'audio' && contentType.includes('audio')) {
456
598
  shouldRecord = true;
457
599
  }
458
- else if (filterType === 'media' && (contentType.includes('video') || contentType.includes('audio') || isStreamAsset)) {
600
+ else if (filterType === 'media' && (contentType.includes('video') || contentType.includes('audio') || isStreamAsset || isVideoAPI)) {
459
601
  shouldRecord = true;
460
602
  }
461
603
  if (shouldRecord) {
@@ -556,19 +698,32 @@ export async function handleNetworkRecordingExtractors(args) {
556
698
  apis: [],
557
699
  };
558
700
  let totalResponses = 0;
559
- const responseHandler = async (response) => {
560
- totalResponses++;
561
- const url = response.url();
562
- const contentType = response.headers()['content-type'] || '';
701
+ const responseHandler = (response) => {
563
702
  try {
564
- // Video files
565
- if (contentType.includes('video') || url.includes('.mp4') || url.includes('.webm')) {
703
+ totalResponses++;
704
+ const url = response.url();
705
+ const contentType = response.headers()['content-type'] || '';
706
+ // Video files (includes API video requests)
707
+ const isVideoFile = contentType.includes('video') ||
708
+ url.includes('.mp4') ||
709
+ url.includes('.webm') ||
710
+ url.includes('.mov') ||
711
+ url.includes('.avi') ||
712
+ url.includes('.mkv');
713
+ // Video API patterns (like cherry.upns.online/api/v1/video)
714
+ const isVideoAPI = url.toLowerCase().includes('/video') ||
715
+ url.toLowerCase().includes('/stream') ||
716
+ url.toLowerCase().includes('/api') ||
717
+ (contentType.includes('application/octet-stream') && url.includes('video'));
718
+ if (isVideoFile || isVideoAPI) {
566
719
  if (verbose)
567
720
  console.log(`[Extractor] 🎥 Video found: ${url.substring(0, 80)}`);
568
721
  extractedData.videos.push({
569
722
  url,
570
723
  contentType,
571
724
  size: response.headers()['content-length'],
725
+ status: response.status(),
726
+ type: isVideoAPI ? 'api' : 'direct',
572
727
  });
573
728
  }
574
729
  // Audio files
@@ -580,30 +735,31 @@ export async function handleNetworkRecordingExtractors(args) {
580
735
  contentType,
581
736
  });
582
737
  }
583
- // Manifest files (HLS, DASH)
738
+ // Manifest files (HLS, DASH) - Don't try to read content in handler
584
739
  if (url.includes('.m3u8') || url.includes('.mpd')) {
585
740
  if (verbose)
586
741
  console.log(`[Extractor] 📜 Manifest found: ${url.substring(0, 80)}`);
587
- const text = await response.text();
588
742
  extractedData.manifests.push({
589
743
  url,
590
744
  type: url.includes('.m3u8') ? 'HLS' : 'DASH',
591
- content: text.substring(0, 500),
745
+ contentType,
746
+ status: response.status(),
592
747
  });
593
748
  }
594
- // API responses with video data
595
- if (contentType.includes('json') && (url.includes('video') || url.includes('media'))) {
749
+ // API responses with video data - Don't try to parse in handler
750
+ if (contentType.includes('json') && (url.includes('video') || url.includes('media') || url.includes('api') || url.includes('player'))) {
596
751
  if (verbose)
597
752
  console.log(`[Extractor] 📡 API found: ${url.substring(0, 80)}`);
598
- const json = await response.json();
599
753
  extractedData.apis.push({
600
754
  url,
601
- data: json,
755
+ contentType,
756
+ status: response.status(),
602
757
  });
603
758
  }
604
759
  }
605
760
  catch (e) {
606
- // Response not available
761
+ if (verbose)
762
+ console.log(`[Extractor] ⚠️ Error processing response: ${e}`);
607
763
  }
608
764
  };
609
765
  console.log(`[Extractor] 🎬 Starting extraction (${duration}ms)${navigateTo ? ` + navigating to ${navigateTo}` : ''}`);
@@ -768,6 +924,7 @@ export async function handleVideosSelectors(args) {
768
924
  const selectors = await page.evaluate(() => {
769
925
  const results = {
770
926
  videoElements: [],
927
+ iframeElements: [],
771
928
  playerContainers: [],
772
929
  controlButtons: [],
773
930
  sources: [],
@@ -781,15 +938,36 @@ export async function handleVideosSelectors(args) {
781
938
  selector,
782
939
  src: video.src,
783
940
  hasControls: video.controls,
941
+ type: 'direct_video'
784
942
  });
785
943
  });
786
- // Player containers
944
+ // Iframe elements (video sources)
945
+ document.querySelectorAll('iframe').forEach((iframe, idx) => {
946
+ const selector = iframe.id ? `#${iframe.id}` :
947
+ iframe.className ? `.${iframe.className.split(' ')[0]}` :
948
+ `iframe:nth-of-type(${idx + 1})`;
949
+ if (iframe.src) {
950
+ results.iframeElements.push({
951
+ selector,
952
+ src: iframe.src,
953
+ title: iframe.title || '',
954
+ allow: iframe.getAttribute('allow') || '',
955
+ type: 'iframe_video'
956
+ });
957
+ }
958
+ });
959
+ // Player containers (check for both video and iframe)
787
960
  ['[class*="player"]', '[id*="player"]', '[data-player]'].forEach(sel => {
788
961
  document.querySelectorAll(sel).forEach((el) => {
962
+ const hasVideo = !!el.querySelector('video');
963
+ const hasIframe = !!el.querySelector('iframe');
789
964
  results.playerContainers.push({
790
965
  selector: sel,
791
966
  id: el.id,
792
967
  className: el.className,
968
+ hasVideo,
969
+ hasIframe,
970
+ contentType: hasVideo ? 'video' : hasIframe ? 'iframe' : 'empty'
793
971
  });
794
972
  });
795
973
  });
@@ -957,43 +1135,143 @@ export async function handleVideoDownloadButtonFinders(args) {
957
1135
  const page = getCurrentPage();
958
1136
  const downloadButtons = await page.evaluate(() => {
959
1137
  const results = [];
1138
+ const foundElements = new Set(); // Avoid duplicates
1139
+ // Enhanced patterns for download buttons
960
1140
  const buttonPatterns = [
961
- 'button[class*="download"]',
962
- 'a[class*="download"]',
1141
+ // Direct download attributes
1142
+ 'a[download]',
1143
+ 'button[download]',
963
1144
  '[data-download]',
964
- 'button:contains("Download")',
965
- 'a:contains("Download")',
966
- 'button:contains("Save")',
1145
+ '[data-download-url]',
1146
+ '[data-file]',
1147
+ '[data-link]',
1148
+ // Class-based
1149
+ 'a[class*="download"]',
1150
+ 'button[class*="download"]',
1151
+ 'a[class*="btn-download"]',
1152
+ '[class*="download-button"]',
1153
+ '[class*="download-link"]',
1154
+ '[class*="dlvideoLinks"]',
1155
+ '[class*="btn-info"]',
1156
+ '[class*="btn-primary"]',
1157
+ // ID-based
1158
+ '[id*="download"]',
1159
+ '[id*="btn-download"]',
1160
+ '[id*="downloadButton"]',
1161
+ '[id*="Download"]',
1162
+ // Href patterns
1163
+ 'a[href*="download"]',
1164
+ 'a[href*=".mp4"]',
1165
+ 'a[href*=".webm"]',
1166
+ 'a[href*=".mkv"]',
1167
+ 'a[href*=".avi"]',
1168
+ 'a[href*="/file/"]',
1169
+ 'a[href*="/stream/"]',
1170
+ 'a[href*="ddn."]',
1171
+ 'a[href*="igx."]',
1172
+ // Onclick patterns
967
1173
  '[onclick*="download"]',
968
- '[href*="download"]',
1174
+ '[onclick*="Download"]',
1175
+ '[onclick*="window.open"]',
1176
+ // Icon-based (common patterns)
1177
+ 'a i[class*="download"]',
1178
+ 'button i[class*="download"]',
1179
+ '.fa-download',
1180
+ '.icon-download',
1181
+ // Form submit buttons
1182
+ 'input[type="submit"][value*="Download"]',
1183
+ 'input[type="submit"][value*="Stream"]',
1184
+ 'button[type="submit"]',
1185
+ ];
1186
+ // Text-based search (case insensitive) - ENHANCED with GDL patterns
1187
+ const searchTexts = [
1188
+ 'download', 'descargar', 'télécharger', 'baixar', 'скачать', 'save', 'get',
1189
+ 'gdl', '5gdl', '4gdl', '3gdl', '2gdl', '1gdl', '⚡5gdl', // GDL variations with lightning
1190
+ 'dl', 'down', 'grab', 'fetch', 'stream', 'watch', 'play', 'click'
969
1191
  ];
970
1192
  buttonPatterns.forEach(pattern => {
971
1193
  try {
972
- document.querySelectorAll(pattern).forEach((btn, idx) => {
1194
+ document.querySelectorAll(pattern).forEach((btn) => {
1195
+ // Avoid duplicates
1196
+ if (foundElements.has(btn))
1197
+ return;
1198
+ foundElements.add(btn);
973
1199
  const isVisible = btn.offsetWidth > 0 && btn.offsetHeight > 0;
1200
+ const text = btn.textContent?.trim() || '';
1201
+ const href = btn.href || btn.getAttribute('href') || '';
974
1202
  results.push({
975
1203
  pattern,
976
- index: idx,
977
- text: btn.textContent?.trim() || '',
978
- href: btn.href || btn.getAttribute('href'),
979
- dataDownload: btn.dataset.download,
1204
+ text,
1205
+ href,
1206
+ dataDownload: btn.dataset.download || btn.getAttribute('data-download'),
980
1207
  isVisible,
981
1208
  tag: btn.tagName.toLowerCase(),
982
1209
  className: btn.className,
983
1210
  id: btn.id,
1211
+ hasDownloadAttr: btn.hasAttribute('download'),
1212
+ onclick: btn.onclick ? 'present' : 'none'
984
1213
  });
985
1214
  });
986
1215
  }
987
1216
  catch (e) {
988
- // Pattern not supported
1217
+ // Pattern not supported or error
1218
+ }
1219
+ });
1220
+ // Additional: Search for buttons/links with download-related text
1221
+ document.querySelectorAll('a, button').forEach((el) => {
1222
+ if (foundElements.has(el))
1223
+ return;
1224
+ const text = el.textContent?.toLowerCase() || '';
1225
+ const hasDownloadText = searchTexts.some(term => text.includes(term));
1226
+ if (hasDownloadText) {
1227
+ foundElements.add(el);
1228
+ const isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;
1229
+ results.push({
1230
+ pattern: 'text-based-search',
1231
+ text: el.textContent?.trim() || '',
1232
+ href: el.href || el.getAttribute('href') || '',
1233
+ dataDownload: el.dataset.download,
1234
+ isVisible,
1235
+ tag: el.tagName.toLowerCase(),
1236
+ className: el.className,
1237
+ id: el.id,
1238
+ matchedText: searchTexts.find(term => text.includes(term))
1239
+ });
989
1240
  }
990
1241
  });
991
1242
  return results;
992
1243
  });
1244
+ // Include option to filter by visibility
1245
+ const includeHidden = args.includeHidden !== false; // Default: include hidden
1246
+ const filteredButtons = includeHidden ? downloadButtons : downloadButtons.filter((btn) => btn.isVisible);
1247
+ // If no buttons found, provide helpful context
1248
+ let additionalInfo = '';
1249
+ if (filteredButtons.length === 0) {
1250
+ const pageContext = await page.evaluate(() => {
1251
+ const allButtons = document.querySelectorAll('button, input[type="submit"], [role="button"]');
1252
+ const allLinks = document.querySelectorAll('a[href]');
1253
+ const allForms = document.querySelectorAll('form');
1254
+ return {
1255
+ totalButtons: allButtons.length,
1256
+ totalLinks: allLinks.length,
1257
+ totalForms: allForms.length,
1258
+ sampleButtons: Array.from(allButtons).slice(0, 5).map((b) => ({
1259
+ text: b.textContent?.trim().substring(0, 50) || '',
1260
+ id: b.id,
1261
+ className: b.className
1262
+ })),
1263
+ sampleLinks: Array.from(allLinks).slice(0, 5).map((l) => ({
1264
+ text: l.textContent?.trim().substring(0, 50) || '',
1265
+ href: l.href?.substring(0, 100) || ''
1266
+ }))
1267
+ };
1268
+ });
1269
+ additionalInfo = `\n\n💡 No download buttons found. Page has:\n • ${pageContext.totalButtons} buttons\n • ${pageContext.totalLinks} links\n • ${pageContext.totalForms} forms\n\nSample elements:\n${JSON.stringify(pageContext, null, 2)}`;
1270
+ }
993
1271
  return {
994
1272
  content: [{
995
1273
  type: 'text',
996
- text: `✅ Found ${downloadButtons.length} download buttons\n\n${JSON.stringify(downloadButtons, null, 2)}`,
1274
+ text: `✅ Found ${filteredButtons.length} download buttons (${downloadButtons.length} total, ${downloadButtons.filter((b) => !b.isVisible).length} hidden)\n\n${JSON.stringify(filteredButtons, null, 2)}${additionalInfo}`,
997
1275
  }],
998
1276
  };
999
1277
  }, 'Failed to find download buttons');
@@ -175,7 +175,7 @@ export async function handleXpathLinks(args) {
175
175
  }, 'Failed to find XPath links');
176
176
  }
177
177
  /**
178
- * AJAX Extractor - Extract AJAX/XHR request data
178
+ * AJAX Extractor - Extract AJAX/XHR request data with responses
179
179
  */
180
180
  export async function handleAjaxExtractor(args) {
181
181
  return await withErrorHandling(async () => {
@@ -186,29 +186,112 @@ export async function handleAjaxExtractor(args) {
186
186
  const page = getCurrentPage();
187
187
  const duration = args.duration || 15000;
188
188
  const url = args.url;
189
+ const forceReload = args.forceReload !== false; // Force reload by default
190
+ const includeResponses = args.includeResponses !== false;
189
191
  const requests = [];
192
+ const responses = [];
190
193
  const requestHandler = (request) => {
191
- const resourceType = request.resourceType();
192
- if (resourceType === 'xhr' || resourceType === 'fetch') {
193
- requests.push({
194
- url: request.url(),
195
- method: request.method(),
196
- resourceType,
197
- headers: request.headers(),
198
- timestamp: new Date().toISOString(),
199
- });
194
+ try {
195
+ const resourceType = request.resourceType();
196
+ if (resourceType === 'xhr' || resourceType === 'fetch') {
197
+ requests.push({
198
+ url: request.url(),
199
+ method: request.method(),
200
+ resourceType,
201
+ headers: request.headers(),
202
+ postData: request.postData(),
203
+ timestamp: new Date().toISOString(),
204
+ });
205
+ }
206
+ }
207
+ catch (e) {
208
+ // Ignore errors
209
+ }
210
+ };
211
+ const responseHandler = async (response) => {
212
+ try {
213
+ const resourceType = response.request().resourceType();
214
+ if (resourceType === 'xhr' || resourceType === 'fetch') {
215
+ let body = null;
216
+ if (includeResponses) {
217
+ try {
218
+ const text = await response.text();
219
+ // Try to parse as JSON
220
+ try {
221
+ body = JSON.parse(text);
222
+ }
223
+ catch {
224
+ body = text.substring(0, 500); // First 500 chars if not JSON
225
+ }
226
+ }
227
+ catch { }
228
+ }
229
+ responses.push({
230
+ url: response.url(),
231
+ status: response.status(),
232
+ statusText: response.statusText(),
233
+ headers: response.headers(),
234
+ body,
235
+ timestamp: new Date().toISOString(),
236
+ });
237
+ }
238
+ }
239
+ catch (e) {
240
+ // Ignore errors
200
241
  }
201
242
  };
202
243
  page.on('request', requestHandler);
244
+ if (includeResponses) {
245
+ page.on('response', responseHandler);
246
+ }
203
247
  if (url && page.url() !== url) {
204
- await page.goto(url, { waitUntil: 'networkidle2' });
248
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
249
+ }
250
+ else if (forceReload && !url) {
251
+ // Force reload current page to capture AJAX requests
252
+ try {
253
+ await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
254
+ }
255
+ catch { }
256
+ }
257
+ // Trigger interactions to generate AJAX requests
258
+ try {
259
+ await page.evaluate(() => {
260
+ // Scroll to trigger lazy loading
261
+ window.scrollTo(0, document.body.scrollHeight / 2);
262
+ window.scrollTo(0, document.body.scrollHeight);
263
+ // Click visible buttons that might trigger AJAX
264
+ const clickableElements = document.querySelectorAll('button, [role="button"], .btn, [onclick]');
265
+ clickableElements.forEach((el) => {
266
+ if (el.offsetWidth > 0 && el.offsetHeight > 0) {
267
+ const text = el.textContent?.toLowerCase() || '';
268
+ // Click safe elements (avoid dangerous buttons like delete, remove, etc.)
269
+ if (text.includes('load') || text.includes('more') || text.includes('show')) {
270
+ try {
271
+ el.click();
272
+ }
273
+ catch { }
274
+ }
275
+ }
276
+ });
277
+ });
205
278
  }
279
+ catch { }
206
280
  await sleep(duration);
207
281
  page.off('request', requestHandler);
282
+ if (includeResponses) {
283
+ page.off('response', responseHandler);
284
+ }
285
+ const combined = {
286
+ totalRequests: requests.length,
287
+ totalResponses: responses.length,
288
+ requests: requests.slice(0, 50), // First 50
289
+ responses: responses.slice(0, 50), // First 50
290
+ };
208
291
  return {
209
292
  content: [{
210
293
  type: 'text',
211
- text: `✅ Captured ${requests.length} AJAX/XHR requests\n\n${JSON.stringify(requests, null, 2)}`,
294
+ text: `✅ Captured ${requests.length} AJAX/XHR requests and ${responses.length} responses\n\n${JSON.stringify(combined, null, 2)}${requests.length === 0 ? '\n\n💡 Tip: Page may not have AJAX requests, or use {"forceReload": true} to capture from page load' : ''}`,
212
295
  }],
213
296
  };
214
297
  }, 'Failed to extract AJAX requests');
@@ -269,40 +352,109 @@ export async function handleNetworkRecorder(args) {
269
352
  const page = getCurrentPage();
270
353
  const duration = args.duration || 20000;
271
354
  const filterTypes = args.filterTypes || ['video', 'xhr', 'fetch', 'media'];
355
+ const navigateUrl = args.navigateUrl; // Optional: Navigate to URL to capture from start
356
+ const clearCache = args.clearCache || false;
357
+ const forceReload = args.forceReload !== false; // Force reload by default to capture events
272
358
  const networkActivity = [];
273
359
  const requestHandler = (request) => {
274
- const resourceType = request.resourceType();
275
- if (filterTypes.includes('all') || filterTypes.includes(resourceType)) {
276
- networkActivity.push({
277
- type: 'request',
278
- url: request.url(),
279
- method: request.method(),
280
- resourceType,
281
- timestamp: new Date().toISOString(),
282
- });
360
+ try {
361
+ const resourceType = request.resourceType();
362
+ if (filterTypes.includes('all') || filterTypes.includes(resourceType)) {
363
+ networkActivity.push({
364
+ type: 'request',
365
+ url: request.url(),
366
+ method: request.method(),
367
+ resourceType,
368
+ headers: request.headers(),
369
+ postData: request.postData(),
370
+ timestamp: new Date().toISOString(),
371
+ });
372
+ }
373
+ }
374
+ catch (e) {
375
+ // Ignore request errors
283
376
  }
284
377
  };
285
- const responseHandler = (response) => {
286
- const resourceType = response.request().resourceType();
287
- if (filterTypes.includes('all') || filterTypes.includes(resourceType)) {
288
- networkActivity.push({
289
- type: 'response',
290
- url: response.url(),
291
- status: response.status(),
292
- resourceType,
293
- timestamp: new Date().toISOString(),
294
- });
378
+ const responseHandler = async (response) => {
379
+ try {
380
+ const resourceType = response.request().resourceType();
381
+ if (filterTypes.includes('all') || filterTypes.includes(resourceType)) {
382
+ const headers = response.headers();
383
+ networkActivity.push({
384
+ type: 'response',
385
+ url: response.url(),
386
+ status: response.status(),
387
+ statusText: response.statusText(),
388
+ resourceType,
389
+ contentType: headers['content-type'] || '',
390
+ contentLength: headers['content-length'] || '',
391
+ timestamp: new Date().toISOString(),
392
+ });
393
+ }
394
+ }
395
+ catch (e) {
396
+ // Ignore response errors
295
397
  }
296
398
  };
399
+ // Start monitoring FIRST
297
400
  page.on('request', requestHandler);
298
401
  page.on('response', responseHandler);
402
+ // Optional: Clear cache for fresh load
403
+ if (clearCache) {
404
+ try {
405
+ const client = await page.target().createCDPSession();
406
+ await client.send('Network.clearBrowserCache');
407
+ await client.detach();
408
+ }
409
+ catch { }
410
+ }
411
+ // Optional: Navigate to URL (capturing from start)
412
+ if (navigateUrl) {
413
+ try {
414
+ await page.goto(navigateUrl, { waitUntil: 'networkidle2', timeout: 30000 });
415
+ }
416
+ catch (e) {
417
+ // Continue monitoring even if navigation fails
418
+ }
419
+ }
420
+ else if (forceReload && !navigateUrl) {
421
+ // Force reload current page to capture network events
422
+ const currentUrl = page.url();
423
+ try {
424
+ await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
425
+ }
426
+ catch (e) {
427
+ // Continue monitoring even if reload fails
428
+ }
429
+ }
430
+ // Also trigger any lazy-loaded content by scrolling
431
+ try {
432
+ await page.evaluate(() => {
433
+ window.scrollTo(0, document.body.scrollHeight / 2);
434
+ window.scrollTo(0, document.body.scrollHeight);
435
+ window.scrollTo(0, 0);
436
+ });
437
+ }
438
+ catch { }
299
439
  await sleep(duration);
300
440
  page.off('request', requestHandler);
301
441
  page.off('response', responseHandler);
442
+ const summary = {
443
+ totalEvents: networkActivity.length,
444
+ requests: networkActivity.filter(e => e.type === 'request').length,
445
+ responses: networkActivity.filter(e => e.type === 'response').length,
446
+ byResourceType: networkActivity.reduce((acc, e) => {
447
+ acc[e.resourceType] = (acc[e.resourceType] || 0) + 1;
448
+ return acc;
449
+ }, {})
450
+ };
451
+ const tipMessage = networkActivity.length === 0 ?
452
+ `\n\n💡 Tips:\n • Page was already loaded. Use {"navigateUrl": "https://example.com"} to capture from start\n • Use {"filterTypes": ["all"]} to capture all network activity\n • Use {"clearCache": true} for fresh page load` :
453
+ '';
302
454
  return {
303
455
  content: [{
304
456
  type: 'text',
305
- text: `✅ Recorded ${networkActivity.length} network events\n\n${JSON.stringify(networkActivity.slice(0, 50), null, 2)}${networkActivity.length > 50 ? '\n\n... (showing first 50)' : ''}`,
457
+ text: `✅ Recorded ${networkActivity.length} network events\n\n📊 Summary:\n${JSON.stringify(summary, null, 2)}\n\nEvents (first 50):\n${JSON.stringify(networkActivity.slice(0, 50), null, 2)}${networkActivity.length > 50 ? '\n\n... (showing first 50 of ' + networkActivity.length + ')' : ''}${tipMessage}`,
306
458
  }],
307
459
  };
308
460
  }, 'Failed to record network');
@@ -384,13 +536,58 @@ export async function handleRegexPatternFinder(args) {
384
536
  }
385
537
  const matches = await page.evaluate(({ pattern, flags }) => {
386
538
  const regex = new RegExp(pattern, flags);
539
+ const results = [];
540
+ // 1. Search in body HTML
387
541
  const html = document.body.innerHTML;
388
- const results = Array.from(html.matchAll(regex));
389
- return results.slice(0, 100).map((match, idx) => ({
390
- index: idx,
391
- match: match[0],
392
- groups: match.slice(1),
393
- }));
542
+ Array.from(html.matchAll(regex)).forEach(match => {
543
+ results.push({
544
+ source: 'html',
545
+ match: match[0],
546
+ groups: match.slice(1),
547
+ index: match.index
548
+ });
549
+ });
550
+ // 2. Search in script tags
551
+ document.querySelectorAll('script').forEach((script, scriptIdx) => {
552
+ const content = script.textContent || '';
553
+ Array.from(content.matchAll(regex)).forEach(match => {
554
+ results.push({
555
+ source: `script[${scriptIdx}]`,
556
+ match: match[0],
557
+ groups: match.slice(1),
558
+ index: match.index,
559
+ scriptSrc: script.src || 'inline'
560
+ });
561
+ });
562
+ });
563
+ // 3. Search in element attributes (href, src, data-* etc.)
564
+ document.querySelectorAll('*').forEach((el) => {
565
+ ['href', 'src', 'data-video', 'data-src', 'data-url'].forEach(attr => {
566
+ const value = el.getAttribute(attr);
567
+ if (value) {
568
+ Array.from(value.matchAll(regex)).forEach(match => {
569
+ results.push({
570
+ source: `attribute[${attr}]`,
571
+ match: match[0],
572
+ groups: match.slice(1),
573
+ element: el.tagName.toLowerCase(),
574
+ attribute: attr,
575
+ fullValue: value
576
+ });
577
+ });
578
+ }
579
+ });
580
+ });
581
+ // Dedupe and limit
582
+ const seen = new Set();
583
+ const unique = results.filter(r => {
584
+ const key = `${r.source}:${r.match}`;
585
+ if (seen.has(key))
586
+ return false;
587
+ seen.add(key);
588
+ return true;
589
+ });
590
+ return unique.slice(0, 100);
394
591
  }, { pattern, flags });
395
592
  return {
396
593
  content: [{
@@ -523,10 +720,15 @@ export async function handleVideoSourceExtractor(args) {
523
720
  });
524
721
  const page = getCurrentPage();
525
722
  const captureDuration = typeof args.captureDuration === 'number' ? args.captureDuration : 6000;
526
- // DOM video elements
527
- const videos = await page.evaluate(() => {
723
+ // DOM video elements + iframe detection
724
+ const videoData = await page.evaluate(() => {
725
+ const results = {
726
+ videos: [],
727
+ iframes: [],
728
+ embeddedPlayers: []
729
+ };
730
+ // 1. Direct video elements
528
731
  const videoElements = document.querySelectorAll('video');
529
- const results = [];
530
732
  videoElements.forEach((video, idx) => {
531
733
  const sources = [];
532
734
  // Direct src
@@ -540,15 +742,49 @@ export async function handleVideoSourceExtractor(args) {
540
742
  type: source.type || 'unknown',
541
743
  });
542
744
  });
543
- results.push({
745
+ results.videos.push({
544
746
  index: idx,
545
747
  poster: video.poster || '',
546
748
  sources,
547
749
  duration: video.duration || 0,
548
750
  width: video.videoWidth || video.width || 0,
549
751
  height: video.videoHeight || video.height || 0,
752
+ type: 'direct_video'
550
753
  });
551
754
  });
755
+ // 2. Iframe video sources
756
+ const iframes = document.querySelectorAll('iframe');
757
+ iframes.forEach((iframe, idx) => {
758
+ if (iframe.src) {
759
+ results.iframes.push({
760
+ index: idx,
761
+ src: iframe.src,
762
+ title: iframe.title || '',
763
+ id: iframe.id,
764
+ className: iframe.className,
765
+ width: iframe.width,
766
+ height: iframe.height,
767
+ type: 'iframe_video'
768
+ });
769
+ }
770
+ });
771
+ // 3. Video players with iframes inside
772
+ const playerContainers = document.querySelectorAll('[class*="player"], [id*="player"], [data-player]');
773
+ playerContainers.forEach((container, idx) => {
774
+ const iframe = container.querySelector('iframe');
775
+ const video = container.querySelector('video');
776
+ if (iframe || video) {
777
+ results.embeddedPlayers.push({
778
+ index: idx,
779
+ hasVideo: !!video,
780
+ hasIframe: !!iframe,
781
+ videoSrc: video ? (video.src || video.currentSrc) : null,
782
+ iframeSrc: iframe ? iframe.src : null,
783
+ containerId: container.id,
784
+ containerClass: container.className
785
+ });
786
+ }
787
+ });
552
788
  return results;
553
789
  });
554
790
  // Network capture for manifests and segments
@@ -584,11 +820,22 @@ export async function handleVideoSourceExtractor(args) {
584
820
  catch { }
585
821
  await sleep(captureDuration);
586
822
  page.off('response', respHandler);
587
- const result = { videos, manifests, segments };
823
+ const result = {
824
+ ...videoData,
825
+ manifests,
826
+ segments,
827
+ summary: {
828
+ totalVideos: videoData.videos.length,
829
+ totalIframes: videoData.iframes.length,
830
+ totalEmbeddedPlayers: videoData.embeddedPlayers.length,
831
+ totalManifests: manifests.length,
832
+ totalSegments: segments.length
833
+ }
834
+ };
588
835
  return {
589
836
  content: [{
590
837
  type: 'text',
591
- text: `✅ Extracted video sources\n\n${JSON.stringify(result, null, 2)}`,
838
+ text: `✅ Extracted video sources\n\n📊 Summary:\n • Direct <video> elements: ${videoData.videos.length}\n • Iframe sources: ${videoData.iframes.length}\n • Embedded players: ${videoData.embeddedPlayers.length}\n • Manifests: ${manifests.length}\n • Segments: ${segments.length}\n\n${JSON.stringify(result, null, 2)}`,
592
839
  }],
593
840
  };
594
841
  }, 'Failed to extract video sources');
@@ -614,20 +861,65 @@ export async function handleVideoPlayerExtractor(args) {
614
861
  ];
615
862
  playerSelectors.forEach(selector => {
616
863
  document.querySelectorAll(selector).forEach((el, idx) => {
617
- const videoEl = el.querySelector('video') || el.querySelector('iframe');
618
- if (videoEl) {
619
- results.push({
864
+ const videoEl = el.querySelector('video');
865
+ const iframeEl = el.querySelector('iframe');
866
+ if (videoEl || iframeEl) {
867
+ const playerInfo = {
620
868
  selector,
621
869
  index: idx,
622
- hasVideo: !!el.querySelector('video'),
623
- hasIframe: !!el.querySelector('iframe'),
624
- src: videoEl.src || videoEl.currentSrc || '',
870
+ hasVideo: !!videoEl,
871
+ hasIframe: !!iframeEl,
625
872
  className: el.className,
626
873
  id: el.id,
627
- });
874
+ };
875
+ // Video element info
876
+ if (videoEl) {
877
+ playerInfo.videoSrc = videoEl.src || videoEl.currentSrc || '';
878
+ playerInfo.videoPoster = videoEl.poster || '';
879
+ playerInfo.videoType = 'direct';
880
+ }
881
+ // Iframe element info
882
+ if (iframeEl) {
883
+ playerInfo.iframeSrc = iframeEl.src || '';
884
+ playerInfo.iframeTitle = iframeEl.title || '';
885
+ playerInfo.iframeAllow = iframeEl.getAttribute('allow') || '';
886
+ playerInfo.videoType = videoEl ? 'hybrid' : 'iframe';
887
+ }
888
+ results.push(playerInfo);
628
889
  }
629
890
  });
630
891
  });
892
+ // Also check standalone iframes (ALL iframes that might be video players)
893
+ document.querySelectorAll('iframe').forEach((iframe, idx) => {
894
+ const src = (iframe.src || '').toLowerCase();
895
+ // Check if iframe is likely a video player
896
+ const isLikelyVideoIframe = src.includes('embed') ||
897
+ src.includes('player') ||
898
+ src.includes('video') ||
899
+ src.includes('stream') ||
900
+ iframe.allow?.includes('autoplay') ||
901
+ iframe.allow?.includes('encrypted-media');
902
+ // Include ALL iframes if they have src and are likely video players
903
+ if (iframe.src && isLikelyVideoIframe) {
904
+ // Check if already added
905
+ const alreadyAdded = results.some(r => r.iframeSrc === iframe.src);
906
+ if (!alreadyAdded) {
907
+ results.push({
908
+ selector: iframe.id ? `#${iframe.id}` : `iframe:nth-of-type(${idx + 1})`,
909
+ index: idx,
910
+ hasVideo: false,
911
+ hasIframe: true,
912
+ iframeSrc: iframe.src,
913
+ iframeTitle: iframe.title || '',
914
+ iframeAllow: iframe.getAttribute('allow') || '',
915
+ className: iframe.className,
916
+ id: iframe.id,
917
+ videoType: 'standalone_iframe',
918
+ isVisible: iframe.offsetWidth > 0 && iframe.offsetHeight > 0
919
+ });
920
+ }
921
+ }
922
+ });
631
923
  return results;
632
924
  });
633
925
  return {
@@ -652,6 +944,7 @@ export async function handleVideoPlayerHosterFinder(args) {
652
944
  const results = [];
653
945
  const iframes = document.querySelectorAll('iframe');
654
946
  const platforms = {
947
+ // Popular platforms
655
948
  'youtube.com': 'YouTube',
656
949
  'youtu.be': 'YouTube',
657
950
  'vimeo.com': 'Vimeo',
@@ -660,6 +953,23 @@ export async function handleVideoPlayerHosterFinder(args) {
660
953
  'twitter.com': 'Twitter',
661
954
  'twitch.tv': 'Twitch',
662
955
  'streamable.com': 'Streamable',
956
+ // Custom video hosting platforms
957
+ 'gdmirrorbot': 'GD Mirror Bot',
958
+ 'multimoviesshg.com': 'MultiMovies StreamHG',
959
+ 'streamhg.com': 'StreamHG',
960
+ 'techinmind.space': 'Tech In Mind Player',
961
+ 'premilkyway.com': 'Premium Milky Way CDN',
962
+ 'p2pplay.pro': 'P2P Play',
963
+ 'rpmhub.site': 'RPM Share',
964
+ 'uns.bio': 'UpnShare',
965
+ 'smoothpre.com': 'EarnVids/SmoothPre',
966
+ 'doodstream.com': 'DoodStream',
967
+ 'streamtape.com': 'StreamTape',
968
+ 'mixdrop.co': 'MixDrop',
969
+ 'upstream.to': 'UpStream',
970
+ 'vidcloud': 'VidCloud',
971
+ 'fembed': 'Fembed',
972
+ 'mp4upload': 'MP4Upload',
663
973
  };
664
974
  iframes.forEach((iframe, idx) => {
665
975
  const src = iframe.src.toLowerCase();
@@ -113,7 +113,17 @@ export class SseTransport {
113
113
  error: errorMessage,
114
114
  timestamp: new Date().toISOString(),
115
115
  });
116
- res.status(500).json({ success: false, error: errorMessage });
116
+ // Return errors in MCP format (status 200 with isError flag)
117
+ res.json({
118
+ success: true,
119
+ result: {
120
+ isError: true,
121
+ content: [{
122
+ type: 'text',
123
+ text: errorMessage
124
+ }]
125
+ }
126
+ });
117
127
  }
118
128
  });
119
129
  // Browser automation endpoints with SSE notifications
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-browser-mcp-server",
3
- "version": "2.12.8",
3
+ "version": "2.12.10",
4
4
  "description": "Universal AI IDE MCP Server - Auto-detects and supports all AI IDEs (Claude Desktop, Cursor, Windsurf, Cline, Zed, VSCode, Qoder AI, etc.) with Brave browser automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,11 +37,11 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.20.2",
40
- "@types/turndown": "^5.0.5",
40
+ "@types/turndown": "^5.0.6",
41
41
  "ajv": "^8.12.0",
42
42
  "axios": "^1.6.5",
43
43
  "brave-real-browser": "^1.5.102",
44
- "brave-real-launcher": "^1.2.18",
44
+ "brave-real-launcher": "^1.2.19",
45
45
  "brave-real-puppeteer-core": "^24.26.1",
46
46
  "cheerio": "^1.0.0-rc.12",
47
47
  "chrono-node": "^2.7.0",