brave-real-browser-mcp-server 2.11.3 → 2.11.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.
@@ -15,13 +15,15 @@ export async function handleVideoLinkFinder(args) {
15
15
  });
16
16
  const page = getCurrentPage();
17
17
  const includeEmbedded = args.includeEmbedded !== false;
18
- const videoLinks = await page.evaluate((includeEmbedded) => {
18
+ const captureDuration = typeof args.captureDuration === 'number' ? args.captureDuration : 7000;
19
+ // 1) Collect DOM-based links quickly
20
+ const domLinks = await page.evaluate((includeEmbedded) => {
19
21
  const results = [];
20
22
  // Direct video links
21
23
  const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv', '.m3u8', '.mpd'];
22
24
  const allLinks = document.querySelectorAll('a[href]');
23
25
  allLinks.forEach((link, idx) => {
24
- const href = link.href.toLowerCase();
26
+ const href = (link.href || '').toLowerCase();
25
27
  videoExtensions.forEach(ext => {
26
28
  if (href.includes(ext)) {
27
29
  results.push({
@@ -46,23 +48,64 @@ export async function handleVideoLinkFinder(args) {
46
48
  });
47
49
  }
48
50
  });
49
- // Embedded videos
51
+ // Embedded videos (iframes)
50
52
  if (includeEmbedded) {
51
- document.querySelectorAll('iframe[src*="video"], iframe[src*="player"]').forEach((iframe, idx) => {
52
- results.push({
53
- index: idx,
54
- url: iframe.src,
55
- type: 'embedded_video',
56
- title: iframe.title || '',
57
- });
53
+ document.querySelectorAll('iframe').forEach((iframe, idx) => {
54
+ if (iframe.src) {
55
+ results.push({
56
+ index: idx,
57
+ url: iframe.src,
58
+ type: 'embedded_video',
59
+ title: iframe.title || '',
60
+ });
61
+ }
58
62
  });
59
63
  }
60
64
  return results;
61
65
  }, includeEmbedded);
66
+ // 2) Network sniff for streaming links (.m3u8/.mpd/.ts/.vtt)
67
+ const streamingLinks = [];
68
+ const respHandler = (response) => {
69
+ try {
70
+ const url = response.url();
71
+ const ct = (response.headers()['content-type'] || '').toLowerCase();
72
+ const isStream = /\.m3u8(\?|$)|\.mpd(\?|$)|\.ts(\?|$)|\.vtt(\?|$)/i.test(url) ||
73
+ ct.includes('application/vnd.apple.mpegurl') || ct.includes('application/x-mpegurl');
74
+ if (isStream) {
75
+ streamingLinks.push({ url, contentType: ct, status: response.status() });
76
+ }
77
+ }
78
+ catch { }
79
+ };
80
+ page.on('response', respHandler);
81
+ // Try to "play" iframe/player so network requests fire
82
+ try {
83
+ const clickPoint = await page.evaluate(() => {
84
+ const iframe = document.querySelector('iframe');
85
+ if (!iframe)
86
+ return null;
87
+ const r = iframe.getBoundingClientRect();
88
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
89
+ });
90
+ if (clickPoint && typeof clickPoint.x === 'number') {
91
+ await page.mouse.click(clickPoint.x, clickPoint.y, { clickCount: 1 });
92
+ }
93
+ }
94
+ catch { }
95
+ await sleep(captureDuration);
96
+ page.off('response', respHandler);
97
+ // Dedupe by URL
98
+ const uniqueStreams = Array.from(new Map(streamingLinks.map(i => [i.url, i])).values());
99
+ const resultSummary = {
100
+ domLinksCount: domLinks.length,
101
+ networkStreamsCount: uniqueStreams.length,
102
+ domLinks,
103
+ streamingLinks: uniqueStreams,
104
+ };
62
105
  return {
63
106
  content: [{
64
107
  type: 'text',
65
- text: `✅ Found ${videoLinks.length} video links\n\n${JSON.stringify(videoLinks, null, 2)}`,
108
+ text: `✅ Video links (DOM + Network)\n\n${JSON.stringify(resultSummary, null, 2)}`,
66
109
  }],
67
110
  };
68
111
  }, 'Failed to find video links');
@@ -402,13 +445,17 @@ export async function handleNetworkRecordingFinder(args) {
402
445
  console.log(`[Network Recording] Processed ${totalResponses} responses, ${matchedResponses} matched`);
403
446
  }
404
447
  let shouldRecord = false;
405
- if (filterType === 'video' && (contentType.includes('video') || resourceType === 'media')) {
448
+ const urlLower = url.toLowerCase();
449
+ const isStreamAsset = /\.m3u8(\?|$)|\.mpd(\?|$)|\.ts(\?|$)|\.vtt(\?|$)|\.mp4(\?|$)|\.webm(\?|$)/i.test(urlLower) ||
450
+ contentType.includes('application/vnd.apple.mpegurl') ||
451
+ contentType.includes('application/x-mpegurl');
452
+ if (filterType === 'video' && (contentType.includes('video') || resourceType === 'media' || isStreamAsset)) {
406
453
  shouldRecord = true;
407
454
  }
408
455
  else if (filterType === 'audio' && contentType.includes('audio')) {
409
456
  shouldRecord = true;
410
457
  }
411
- else if (filterType === 'media' && (contentType.includes('video') || contentType.includes('audio'))) {
458
+ else if (filterType === 'media' && (contentType.includes('video') || contentType.includes('audio') || isStreamAsset)) {
412
459
  shouldRecord = true;
413
460
  }
414
461
  if (shouldRecord) {
@@ -611,6 +658,8 @@ export async function handleVideoLinksFinders(args) {
611
658
  requirePage: true,
612
659
  });
613
660
  const page = getCurrentPage();
661
+ const captureDuration = typeof args.captureDuration === 'number' ? args.captureDuration : 7000;
662
+ // DOM discovery first
614
663
  const videoLinks = await page.evaluate(() => {
615
664
  const results = {
616
665
  directLinks: [],
@@ -620,7 +669,7 @@ export async function handleVideoLinksFinders(args) {
620
669
  };
621
670
  // Direct video links
622
671
  document.querySelectorAll('a[href]').forEach((link) => {
623
- const href = link.href.toLowerCase();
672
+ const href = (link.href || '').toLowerCase();
624
673
  if (href.includes('.mp4') || href.includes('.webm') || href.includes('.mov')) {
625
674
  results.directLinks.push({
626
675
  url: link.href,
@@ -630,25 +679,24 @@ export async function handleVideoLinksFinders(args) {
630
679
  });
631
680
  // Embedded iframes
632
681
  document.querySelectorAll('iframe').forEach((iframe) => {
633
- const src = iframe.src.toLowerCase();
634
- if (src.includes('youtube') || src.includes('vimeo') || src.includes('video')) {
682
+ if (iframe.src) {
635
683
  results.embeddedLinks.push({
636
684
  url: iframe.src,
637
685
  title: iframe.title,
638
686
  });
639
687
  }
640
688
  });
641
- // Streaming manifests
689
+ // Streaming manifests present in inline scripts
642
690
  const scripts = Array.from(document.querySelectorAll('script'));
643
691
  scripts.forEach(script => {
644
692
  const content = script.textContent || '';
645
693
  const m3u8Match = content.match(/https?:\/\/[^\s"']+\.m3u8/g);
646
694
  const mpdMatch = content.match(/https?:\/\/[^\s"']+\.mpd/g);
647
695
  if (m3u8Match) {
648
- m3u8Match.forEach(url => results.streamingLinks.push({ url, type: 'HLS' }));
696
+ m3u8Match.forEach(url => results.streamingLinks.push({ url, type: 'HLS', source: 'inline' }));
649
697
  }
650
698
  if (mpdMatch) {
651
- mpdMatch.forEach(url => results.streamingLinks.push({ url, type: 'DASH' }));
699
+ mpdMatch.forEach(url => results.streamingLinks.push({ url, type: 'DASH', source: 'inline' }));
652
700
  }
653
701
  });
654
702
  // Video player links
@@ -663,10 +711,46 @@ export async function handleVideoLinksFinders(args) {
663
711
  });
664
712
  return results;
665
713
  });
714
+ // Network enrichment (m3u8/mpd/ts/vtt)
715
+ const networkStreams = [];
716
+ const respHandler = (response) => {
717
+ try {
718
+ const url = response.url();
719
+ const ct = (response.headers()['content-type'] || '').toLowerCase();
720
+ if (/\.m3u8(\?|$)|\.mpd(\?|$)|\.ts(\?|$)|\.vtt(\?|$)/i.test(url) ||
721
+ ct.includes('application/vnd.apple.mpegurl') || ct.includes('application/x-mpegurl')) {
722
+ const type = url.includes('.mpd') ? 'DASH' : url.includes('.m3u8') ? 'HLS' : 'segment';
723
+ networkStreams.push({ url, type, contentType: ct, status: response.status(), source: 'network' });
724
+ }
725
+ }
726
+ catch { }
727
+ };
728
+ page.on('response', respHandler);
729
+ // Nudge the player by clicking the visible iframe center (if any)
730
+ try {
731
+ const clickPoint = await page.evaluate(() => {
732
+ const iframe = document.querySelector('iframe');
733
+ if (!iframe)
734
+ return null;
735
+ const r = iframe.getBoundingClientRect();
736
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
737
+ });
738
+ if (clickPoint && typeof clickPoint.x === 'number') {
739
+ await page.mouse.click(clickPoint.x, clickPoint.y, { clickCount: 1 });
740
+ }
741
+ }
742
+ catch { }
743
+ await sleep(captureDuration);
744
+ page.off('response', respHandler);
745
+ // Merge + dedupe
746
+ const merged = {
747
+ ...videoLinks,
748
+ streamingLinks: Array.from(new Map([...videoLinks.streamingLinks, ...networkStreams].map((i) => [i.url, i])).values()),
749
+ };
666
750
  return {
667
751
  content: [{
668
752
  type: 'text',
669
- text: `✅ Video Links Found\n\n${JSON.stringify(videoLinks, null, 2)}`,
753
+ text: `✅ Video Links Found\n\n${JSON.stringify(merged, null, 2)}`,
670
754
  }],
671
755
  };
672
756
  }, 'Failed to find video links');
@@ -317,8 +317,9 @@ export async function handleApiFinder(args) {
317
317
  requirePage: true,
318
318
  });
319
319
  const page = getCurrentPage();
320
- const deepScan = args.deepScan !== false;
321
- const apis = await page.evaluate(({ deepScan }) => {
320
+ const captureDuration = typeof args.duration === 'number' ? args.duration : 8000;
321
+ // From inline scripts
322
+ const scriptApis = await page.evaluate(() => {
322
323
  const results = [];
323
324
  const scripts = Array.from(document.querySelectorAll('script'));
324
325
  const apiPatterns = [
@@ -333,32 +334,35 @@ export async function handleApiFinder(args) {
333
334
  apiPatterns.forEach(pattern => {
334
335
  const matches = content.match(pattern);
335
336
  if (matches) {
336
- matches.forEach(match => results.push({
337
- url: match,
338
- source: 'script',
339
- }));
337
+ matches.forEach(match => results.push({ url: match, source: 'script' }));
340
338
  }
341
339
  });
342
340
  });
343
- // Deep scan: also check HTML content for API references
344
- if (deepScan) {
345
- const htmlContent = document.body.innerHTML;
346
- apiPatterns.forEach(pattern => {
347
- const matches = htmlContent.match(pattern);
348
- if (matches) {
349
- matches.forEach(match => results.push({
350
- url: match,
351
- source: 'html_content',
352
- }));
353
- }
354
- });
341
+ return results;
342
+ });
343
+ // From network (XHR/fetch)
344
+ const networkApis = [];
345
+ const respHandler = async (response) => {
346
+ try {
347
+ const req = response.request();
348
+ const rt = req.resourceType();
349
+ const url = response.url();
350
+ const ct = (response.headers()['content-type'] || '').toLowerCase();
351
+ if ((rt === 'xhr' || rt === 'fetch') && (ct.includes('json') || /\/api\//.test(url))) {
352
+ networkApis.push({ url, status: response.status(), method: req.method(), source: 'network' });
353
+ }
355
354
  }
356
- return [...new Set(results.map(r => r.url))].map(url => ({ url, source: 'script' }));
357
- }, { deepScan });
355
+ catch { }
356
+ };
357
+ page.on('response', respHandler);
358
+ await sleep(captureDuration);
359
+ page.off('response', respHandler);
360
+ const all = [...scriptApis, ...networkApis];
361
+ const dedup = Array.from(new Map(all.map(i => [i.url, i])).values());
358
362
  return {
359
363
  content: [{
360
364
  type: 'text',
361
- text: `✅ Found ${apis.length} API endpoints\n\n${JSON.stringify(apis, null, 2)}`,
365
+ text: `✅ Found ${dedup.length} API endpoints\n\n${JSON.stringify(dedup, null, 2)}`,
362
366
  }],
363
367
  };
364
368
  }, 'Failed to find APIs');
@@ -518,6 +522,8 @@ export async function handleVideoSourceExtractor(args) {
518
522
  requirePage: true,
519
523
  });
520
524
  const page = getCurrentPage();
525
+ const captureDuration = typeof args.captureDuration === 'number' ? args.captureDuration : 6000;
526
+ // DOM video elements
521
527
  const videos = await page.evaluate(() => {
522
528
  const videoElements = document.querySelectorAll('video');
523
529
  const results = [];
@@ -545,10 +551,44 @@ export async function handleVideoSourceExtractor(args) {
545
551
  });
546
552
  return results;
547
553
  });
554
+ // Network capture for manifests and segments
555
+ const manifests = [];
556
+ const segments = [];
557
+ const respHandler = async (response) => {
558
+ try {
559
+ const url = response.url();
560
+ const ct = (response.headers()['content-type'] || '').toLowerCase();
561
+ if (/\.m3u8(\?|$)|\.mpd(\?|$)/i.test(url) || ct.includes('application/vnd.apple.mpegurl') || ct.includes('application/x-mpegurl')) {
562
+ const content = await response.text().catch(() => '');
563
+ manifests.push({ url, type: url.includes('.mpd') ? 'DASH' : 'HLS', status: response.status(), content: content.slice(0, 2000) });
564
+ }
565
+ else if (/\.ts(\?|$)|\.m4s(\?|$)|\.mp4(\?|$)/i.test(url)) {
566
+ segments.push({ url, status: response.status(), size: response.headers()['content-length'] });
567
+ }
568
+ }
569
+ catch { }
570
+ };
571
+ page.on('response', respHandler);
572
+ // Best-effort: click center of first iframe to trigger playback
573
+ try {
574
+ const pt = await page.evaluate(() => {
575
+ const ifr = document.querySelector('iframe');
576
+ if (!ifr)
577
+ return null;
578
+ const r = ifr.getBoundingClientRect();
579
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
580
+ });
581
+ if (pt)
582
+ await page.mouse.click(pt.x, pt.y);
583
+ }
584
+ catch { }
585
+ await sleep(captureDuration);
586
+ page.off('response', respHandler);
587
+ const result = { videos, manifests, segments };
548
588
  return {
549
589
  content: [{
550
590
  type: 'text',
551
- text: `✅ Extracted ${videos.length} video sources\n\n${JSON.stringify(videos, null, 2)}`,
591
+ text: `✅ Extracted video sources\n\n${JSON.stringify(result, null, 2)}`,
552
592
  }],
553
593
  };
554
594
  }, 'Failed to extract video sources');
@@ -655,6 +695,7 @@ export async function handleOriginalVideoHosterFinder(args) {
655
695
  requirePage: true,
656
696
  });
657
697
  const page = getCurrentPage();
698
+ const captureDuration = typeof args.captureDuration === 'number' ? args.captureDuration : 6000;
658
699
  const videoData = await page.evaluate(() => {
659
700
  const results = {
660
701
  directVideos: [],
@@ -665,33 +706,58 @@ export async function handleOriginalVideoHosterFinder(args) {
665
706
  document.querySelectorAll('video').forEach((video) => {
666
707
  const src = video.src || video.currentSrc;
667
708
  if (src) {
668
- results.directVideos.push({
669
- src,
670
- type: 'direct',
671
- poster: video.poster,
672
- });
709
+ results.directVideos.push({ src, type: 'direct', poster: video.poster });
673
710
  }
674
711
  video.querySelectorAll('source').forEach((source) => {
675
- results.directVideos.push({
676
- src: source.src,
677
- type: source.type,
678
- quality: source.dataset.quality || 'unknown',
679
- });
712
+ if (source.src) {
713
+ results.directVideos.push({ src: source.src, type: source.type, quality: source.dataset.quality || 'unknown' });
714
+ }
680
715
  });
681
716
  });
682
717
  // Iframe videos
683
- document.querySelectorAll('iframe[src*="video"], iframe[src*="player"]').forEach((iframe) => {
684
- results.iframeVideos.push({
685
- src: iframe.src,
686
- type: 'iframe',
687
- });
718
+ document.querySelectorAll('iframe').forEach((iframe) => {
719
+ if (iframe.src) {
720
+ results.iframeVideos.push({ src: iframe.src, type: 'iframe' });
721
+ }
688
722
  });
689
723
  return results;
690
724
  });
725
+ // Network-derived hosts (m3u8/mpd)
726
+ const hosts = new Set();
727
+ const respHandler = (response) => {
728
+ try {
729
+ const url = response.url();
730
+ if (/\.m3u8(\?|$)|\.mpd(\?|$)/i.test(url)) {
731
+ try {
732
+ hosts.add(new URL(url).hostname);
733
+ }
734
+ catch { }
735
+ }
736
+ }
737
+ catch { }
738
+ };
739
+ page.on('response', respHandler);
740
+ // Kick the player once
741
+ try {
742
+ const pt = await page.evaluate(() => {
743
+ const ifr = document.querySelector('iframe');
744
+ if (!ifr)
745
+ return null;
746
+ const r = ifr.getBoundingClientRect();
747
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
748
+ });
749
+ if (pt)
750
+ await page.mouse.click(pt.x, pt.y);
751
+ }
752
+ catch { }
753
+ await sleep(captureDuration);
754
+ page.off('response', respHandler);
755
+ const possibleSources = Array.from(hosts).map(h => ({ host: h }));
756
+ const enriched = { ...videoData, possibleSources };
691
757
  return {
692
758
  content: [{
693
759
  type: 'text',
694
- text: `✅ Video sources found\n\n${JSON.stringify(videoData, null, 2)}`,
760
+ text: `✅ Video sources found\n\n${JSON.stringify(enriched, null, 2)}`,
695
761
  }],
696
762
  };
697
763
  }, 'Failed to find original video hoster');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-browser-mcp-server",
3
- "version": "2.11.3",
3
+ "version": "2.11.5",
4
4
  "description": "MCP server for brave-real-browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",