brave-real-browser-mcp-server 2.20.0 → 2.21.0

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.
@@ -298,13 +298,33 @@ export async function initializeBrowser(options) {
298
298
  // console.(' Mode: GUI (visible browser)');
299
299
  }
300
300
  // Brave-real-browser handles everything automatically
301
- // Simply pass-through user options without any manual args/flags
301
+ // Enable uBlock Origin for ad/popup blocking
302
+ // Add adblocker plugin for additional protection
303
+ // Dynamically import adblocker plugin
304
+ let adblockerPlugin = null;
305
+ try {
306
+ const adblockerModule = await import('puppeteer-extra-plugin-adblocker');
307
+ const AdblockerPlugin = (adblockerModule.default || adblockerModule);
308
+ if (typeof AdblockerPlugin === 'function') {
309
+ adblockerPlugin = AdblockerPlugin({
310
+ blockTrackers: true,
311
+ blockTrackersAndAnnoyances: true,
312
+ });
313
+ }
314
+ }
315
+ catch (e) {
316
+ // Adblocker plugin not available, continue without it
317
+ }
302
318
  const connectOptions = {
303
319
  headless: headlessMode,
304
320
  turnstile: true,
305
321
  connectOption: {
306
322
  defaultViewport: null // Full window content, no viewport restriction
307
323
  },
324
+ plugins: adblockerPlugin ? [adblockerPlugin] : [], // Add adblocker plugin
325
+ customConfig: {
326
+ autoLoadUBlock: true, // Enable uBlock Origin in brave-real-launcher
327
+ },
308
328
  ...options, // Pass-through all user options
309
329
  };
310
330
  // Ensure headless is set correctly (overriding any conflicting option)
@@ -54,15 +54,31 @@ export async function handleBreadcrumbNavigator(page, args) {
54
54
  export async function handleUrlRedirectTracer(page, args) {
55
55
  const maxRedirects = args.maxRedirects || 10;
56
56
  const chain = [];
57
- await page.setRequestInterception(true);
58
57
  const redirectHandler = (request) => {
59
- if (request.isNavigationRequest()) {
60
- chain.push(request.url());
58
+ try {
59
+ // Check if request is already handled
60
+ if (request.isInterceptResolutionHandled && request.isInterceptResolutionHandled()) {
61
+ return;
62
+ }
63
+ if (request.isNavigationRequest()) {
64
+ chain.push(request.url());
65
+ }
66
+ // Only continue if not already handled
67
+ if (!request.isInterceptResolutionHandled || !request.isInterceptResolutionHandled()) {
68
+ request.continue().catch(() => { });
69
+ }
70
+ }
71
+ catch (e) {
72
+ // Ignore errors in handler
61
73
  }
62
- request.continue();
63
74
  };
64
75
  try {
65
- await page.setRequestInterception(true);
76
+ try {
77
+ await page.setRequestInterception(true);
78
+ }
79
+ catch (e) {
80
+ // Interception might already be enabled
81
+ }
66
82
  page.on('request', redirectHandler);
67
83
  await page.goto(args.url, { waitUntil: 'networkidle2' });
68
84
  chain.push(page.url());
@@ -515,31 +531,50 @@ export async function handleNetworkRecorder(page, args) {
515
531
  const requests = [];
516
532
  const duration = args.duration || 10000;
517
533
  let totalSize = 0;
518
- const requestHandler = async (request) => {
519
- const url = request.url();
520
- if (args.filterUrl && !url.includes(args.filterUrl)) {
521
- request.continue();
522
- return;
523
- }
524
- const entry = {
525
- url,
526
- method: request.method(),
527
- resourceType: request.resourceType(),
528
- timestamp: Date.now(),
529
- };
530
- if (args.includeHeaders) {
531
- entry.headers = request.headers();
534
+ const requestHandler = (request) => {
535
+ try {
536
+ // Check if request is already handled to prevent crash
537
+ if (request.isInterceptResolutionHandled && request.isInterceptResolutionHandled()) {
538
+ return;
539
+ }
540
+ const url = request.url();
541
+ if (args.filterUrl && !url.includes(args.filterUrl)) {
542
+ if (!request.isInterceptResolutionHandled || !request.isInterceptResolutionHandled()) {
543
+ request.continue().catch(() => { });
544
+ }
545
+ return;
546
+ }
547
+ const entry = {
548
+ url,
549
+ method: request.method(),
550
+ resourceType: request.resourceType(),
551
+ timestamp: Date.now(),
552
+ };
553
+ if (args.includeHeaders) {
554
+ entry.headers = request.headers();
555
+ }
556
+ if (args.includeBody) {
557
+ entry.postData = request.postData();
558
+ }
559
+ requests.push(entry);
560
+ // Only continue if not already handled
561
+ if (!request.isInterceptResolutionHandled || !request.isInterceptResolutionHandled()) {
562
+ request.continue().catch(() => { });
563
+ }
532
564
  }
533
- if (args.includeBody) {
534
- entry.postData = request.postData();
565
+ catch {
566
+ // Ignore all errors in handler to prevent crash
535
567
  }
536
- requests.push(entry);
537
- request.continue();
538
568
  };
539
569
  try {
540
- await page.setRequestInterception(true);
570
+ try {
571
+ await page.setRequestInterception(true);
572
+ }
573
+ catch (e) {
574
+ // Interception might already be enabled
575
+ }
541
576
  page.on('request', requestHandler);
542
- page.on('response', async (response) => {
577
+ page.on('response', (response) => {
543
578
  try {
544
579
  const headers = response.headers();
545
580
  const size = parseInt(headers['content-length'] || '0', 10);
@@ -551,6 +586,9 @@ export async function handleNetworkRecorder(page, args) {
551
586
  });
552
587
  await new Promise((r) => setTimeout(r, duration));
553
588
  }
589
+ catch (e) {
590
+ // Capture setup errors
591
+ }
554
592
  finally {
555
593
  page.off('request', requestHandler);
556
594
  try {
@@ -571,17 +609,29 @@ export async function handleApiFinder(page, args) {
571
609
  const apis = [];
572
610
  const patterns = args.patterns || ['/api/', '/v1/', '/v2/', '/graphql', '/rest/', '.json'];
573
611
  const requestHandler = (request) => {
574
- const url = request.url();
575
- const isApi = patterns.some((p) => url.includes(p));
576
- const isXhr = request.resourceType() === 'xhr' || request.resourceType() === 'fetch';
577
- if (isApi || (args.includeInternal !== false && isXhr)) {
578
- apis.push({
579
- url,
580
- method: request.method(),
581
- type: request.resourceType(),
582
- });
612
+ try {
613
+ // Check if request is already handled
614
+ if (request.isInterceptResolutionHandled && request.isInterceptResolutionHandled()) {
615
+ return;
616
+ }
617
+ const url = request.url();
618
+ const isApi = patterns.some((p) => url.includes(p));
619
+ const isXhr = request.resourceType() === 'xhr' || request.resourceType() === 'fetch';
620
+ if (isApi || (args.includeInternal !== false && isXhr)) {
621
+ apis.push({
622
+ url,
623
+ method: request.method(),
624
+ type: request.resourceType(),
625
+ });
626
+ }
627
+ // Only continue if not already handled
628
+ if (!request.isInterceptResolutionHandled || !request.isInterceptResolutionHandled()) {
629
+ request.continue().catch(() => { });
630
+ }
631
+ }
632
+ catch (e) {
633
+ // Ignore errors in handler
583
634
  }
584
- request.continue();
585
635
  };
586
636
  try {
587
637
  await page.setRequestInterception(true);
@@ -1325,11 +1375,23 @@ export async function handleM3u8Parser(page, args) {
1325
1375
  // Intercept network requests to find m3u8 files
1326
1376
  const m3u8Urls = [];
1327
1377
  const requestHandler = (request) => {
1328
- const url = request.url();
1329
- if (url.includes('.m3u8') || url.includes('manifest') || url.includes('playlist')) {
1330
- m3u8Urls.push(url);
1378
+ try {
1379
+ // Check if request is already handled
1380
+ if (request.isInterceptResolutionHandled && request.isInterceptResolutionHandled()) {
1381
+ return;
1382
+ }
1383
+ const url = request.url();
1384
+ if (url.includes('.m3u8') || url.includes('manifest') || url.includes('playlist')) {
1385
+ m3u8Urls.push(url);
1386
+ }
1387
+ // Only continue if not already handled
1388
+ if (!request.isInterceptResolutionHandled || !request.isInterceptResolutionHandled()) {
1389
+ request.continue().catch(() => { });
1390
+ }
1391
+ }
1392
+ catch (e) {
1393
+ // Ignore errors in handler
1331
1394
  }
1332
- request.continue();
1333
1395
  };
1334
1396
  try {
1335
1397
  await page.setRequestInterception(true);
@@ -85,145 +85,48 @@ export async function handleFindSelector(args) {
85
85
  // Enhanced selector finding with authentication detection
86
86
  const results = await pageInstance.evaluate((searchText, selectors, isExact) => {
87
87
  const elements = [];
88
- // Authentication patterns for special handling
89
- const authPatterns = [
90
- /^(log\s*in|sign\s*in|log\s*on|sign\s*on)$/i,
91
- /^(login|signin|authenticate|enter)$/i,
92
- /continue with (google|github|facebook|twitter|microsoft)/i,
93
- /sign in with/i
94
- ];
95
- const isAuthSearch = authPatterns.some(pattern => pattern.test(searchText));
96
- // Utility class patterns to ignore (but not remove completely)
97
- const utilityPatterns = [
98
- /^(m|p|mt|mb|ml|mr|pt|pb|pl|pr|mx|my|px|py)-?\d+$/,
99
- /^(text|bg|border)-(primary|secondary|danger|warning|info|success|light|dark|white|black)$/,
100
- /^(d|display)-(none|block|inline|flex|grid)$/,
101
- /^(w|h)-\d+$/,
102
- /^(btn|button)-(sm|md|lg|xl)$/
103
- ];
104
- function isUtilityClass(className) {
105
- return utilityPatterns.some(pattern => pattern.test(className));
106
- }
107
- function isMeaningfulClass(className) {
108
- // Keep classes that seem semantic/meaningful
109
- const meaningfulPatterns = [
110
- /^(nav|menu|header|footer|sidebar|content|main|article)/,
111
- /^(form|input|button|link|modal|dialog)/,
112
- /^(auth|login|signin|signup|register)/,
113
- /^(search|filter|sort|toggle)/,
114
- /(container|wrapper|section|panel|card)$/
115
- ];
116
- return meaningfulPatterns.some(pattern => pattern.test(className.toLowerCase()));
117
- }
118
- function generateSimpleSelector(element) {
119
- // Prioritize ID
120
- if (element.id && /^[a-zA-Z][\w-]*$/.test(element.id)) {
121
- return `#${CSS.escape(element.id)}`;
122
- }
123
- // Try data attributes
124
- const dataAttrs = Array.from(element.attributes)
125
- .filter(attr => attr.name.startsWith('data-') && attr.value)
126
- .map(attr => `[${attr.name}="${CSS.escape(attr.value)}"]`);
127
- if (dataAttrs.length > 0) {
128
- return element.tagName.toLowerCase() + dataAttrs[0];
129
- }
130
- // Use meaningful classes
131
- if (element.className && typeof element.className === 'string') {
132
- const classes = element.className.trim().split(/\s+/)
133
- .filter(cls => cls && (isMeaningfulClass(cls) || !isUtilityClass(cls)))
134
- .slice(0, 2); // Limit to 2 classes for simplicity
135
- if (classes.length > 0) {
136
- return element.tagName.toLowerCase() + '.' + classes.map(c => CSS.escape(c)).join('.');
137
- }
138
- }
139
- // Fallback to tag + text content for small text
140
- const textContent = element.textContent?.trim() || '';
141
- if (textContent.length > 0 && textContent.length <= 30) {
142
- return `${element.tagName.toLowerCase()}:contains("${textContent}")`;
143
- }
144
- return element.tagName.toLowerCase();
145
- }
146
- function calculateElementScore(element, searchText) {
147
- let score = 0;
148
- const elementText = element.textContent?.trim() || '';
149
- const lowerSearchText = searchText.toLowerCase();
150
- const lowerElementText = elementText.toLowerCase();
151
- // Exact match bonus
152
- if (lowerElementText === lowerSearchText)
153
- score += 100;
154
- // Contains match
155
- else if (lowerElementText.includes(lowerSearchText))
156
- score += 50;
157
- // Word boundary match bonus
158
- const wordRegex = new RegExp(`\\b${lowerSearchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
159
- if (wordRegex.test(lowerElementText))
160
- score += 25;
161
- // Interactive elements bonus
162
- if (['button', 'a', 'input'].includes(element.tagName.toLowerCase()))
163
- score += 20;
164
- // Role attribute bonus
165
- if (element.getAttribute('role'))
166
- score += 10;
167
- // ID bonus
168
- if (element.id)
169
- score += 15;
170
- // Clickable bonus
171
- if (element.getAttribute('onclick') || element.getAttribute('href'))
172
- score += 10;
173
- // Penalize utility classes
174
- if (element.className && typeof element.className === 'string') {
175
- const utilityCount = element.className.split(/\s+/).filter(isUtilityClass).length;
176
- score -= utilityCount * 5;
177
- }
178
- return score;
179
- }
88
+ const lowerSearchText = searchText.toLowerCase();
180
89
  // Search through specified selectors
181
90
  for (const baseSelector of selectors) {
182
91
  const candidates = document.querySelectorAll(baseSelector);
183
92
  candidates.forEach(element => {
184
93
  const elementText = element.textContent?.trim() || '';
185
- const ariaLabel = element.getAttribute('aria-label') || '';
186
- const title = element.getAttribute('title') || '';
187
- const placeholder = element.getAttribute('placeholder') || '';
188
- const searchableText = [elementText, ariaLabel, title, placeholder].join(' ').toLowerCase();
189
- const lowerSearchText = searchText.toLowerCase();
94
+ const lowerElementText = elementText.toLowerCase();
190
95
  let matches = false;
191
96
  if (isExact) {
192
- matches = elementText.toLowerCase() === lowerSearchText ||
193
- ariaLabel.toLowerCase() === lowerSearchText;
97
+ matches = lowerElementText === lowerSearchText;
194
98
  }
195
99
  else {
196
- matches = searchableText.includes(lowerSearchText);
197
- }
198
- // Special handling for authentication searches
199
- if (isAuthSearch && !matches) {
200
- const href = element.href || '';
201
- const hasAuthRoute = href.includes('login') || href.includes('signin') ||
202
- href.includes('auth') || href.includes('oauth');
203
- if (hasAuthRoute)
204
- matches = true;
100
+ matches = lowerElementText.includes(lowerSearchText);
205
101
  }
206
102
  if (matches) {
207
- const rect = element.getBoundingClientRect();
208
- const selector = generateSimpleSelector(element);
209
- const confidence = calculateElementScore(element, searchText);
103
+ // Generate simple selector
104
+ let selector = element.tagName.toLowerCase();
105
+ if (element.id) {
106
+ selector += '#' + element.id;
107
+ }
108
+ else if (element.className && typeof element.className === 'string') {
109
+ const cls = element.className.trim().split(/\s+/)[0];
110
+ if (cls)
111
+ selector += '.' + cls;
112
+ }
113
+ // Calculate dummy confidence / score based on match
114
+ let confidence = 100;
115
+ if (lowerElementText === lowerSearchText)
116
+ confidence = 100;
117
+ else
118
+ confidence = 80;
210
119
  elements.push({
211
120
  selector,
212
- text: elementText,
121
+ text: elementText.substring(0, 100),
213
122
  tagName: element.tagName.toLowerCase(),
214
- confidence,
215
- rect: {
216
- x: rect.x,
217
- y: rect.y,
218
- width: rect.width,
219
- height: rect.height
220
- }
123
+ confidence
221
124
  });
222
125
  }
223
126
  });
224
127
  }
225
- // Sort by confidence score
226
- return elements.sort((a, b) => b.confidence - a.confidence);
128
+ // Return top 10 unique
129
+ return elements.slice(0, 10);
227
130
  }, text, // Use the text argument from args
228
131
  searchSelectors, exact);
229
132
  if (results.length === 0) {
@@ -117,17 +117,8 @@ export async function handleSaveContentAsMarkdown(args) {
117
117
  catch (error) {
118
118
  throw new Error(`Failed to create directory: ${dirPath}. ${error instanceof Error ? error.message : String(error)}`);
119
119
  }
120
- // Check if file already exists
121
- try {
122
- await fs.access(filePath);
123
- throw new Error(`File already exists: ${filePath}. Please choose a different path or delete the existing file.`);
124
- }
125
- catch (error) {
126
- // File doesn't exist, which is what we want
127
- if (error.code !== 'ENOENT') {
128
- throw error;
129
- }
130
- }
120
+ // File existence check removed to allow overwriting
121
+ // ensure directory is there (already checked above)
131
122
  // Extract content from page
132
123
  let content;
133
124
  let currentUrl;
@@ -152,20 +152,40 @@ async function waitForCloudflareBypass(pageInstance) {
152
152
  try {
153
153
  // Initial stable wait
154
154
  await new Promise(resolve => setTimeout(resolve, 2000));
155
- const maxWait = 40000;
155
+ const maxWait = 60000;
156
156
  const startTime = Date.now();
157
157
  while (Date.now() - startTime < maxWait) {
158
158
  const isChallenge = await pageInstance.evaluate(() => {
159
159
  const bodyText = (document.body?.innerText || '').toLowerCase();
160
160
  // Strict checks to avoid false positives on normal sites
161
- return bodyText.includes('verifying you are human') ||
162
- bodyText.includes('checking your browser before accessing');
161
+ const hasChallengeText = bodyText.includes('verifying you are human') ||
162
+ bodyText.includes('checking your browser before accessing') ||
163
+ bodyText.includes('just a moment');
164
+ // Also check for challenge iframes
165
+ const hasChallengeFrames = !!document.querySelector('iframe[src*="cloudflare"]') ||
166
+ !!document.querySelector('iframe[src*="turnstile"]');
167
+ return hasChallengeText && hasChallengeFrames;
163
168
  });
164
169
  if (!isChallenge) {
165
- return; // Not a challenge page, or passed
170
+ // Double check state remains stable
171
+ await new Promise(resolve => setTimeout(resolve, 1500));
172
+ const stillChallenge = await pageInstance.evaluate(() => {
173
+ const bodyText = (document.body?.innerText || '').toLowerCase();
174
+ return bodyText.includes('verifying you are human');
175
+ });
176
+ if (!stillChallenge)
177
+ return; // Not a challenge page, or passed
178
+ }
179
+ // Simulate human behavior: Random small mouse movements
180
+ try {
181
+ // Move mouse slightly to simulate presence
182
+ await pageInstance.mouse.move(100 + Math.random() * 200, 100 + Math.random() * 200, { steps: 5 });
183
+ }
184
+ catch (e) {
185
+ // Ignore mouse errors (e.g. if target closed)
166
186
  }
167
187
  // Still blocked, wait
168
- await new Promise(resolve => setTimeout(resolve, 1000));
188
+ await new Promise(resolve => setTimeout(resolve, 2000));
169
189
  }
170
190
  }
171
191
  catch (error) {
@@ -66,7 +66,14 @@ export class WorkflowValidator {
66
66
  'type': [WorkflowState.CONTENT_ANALYZED, WorkflowState.SELECTOR_AVAILABLE],
67
67
  'wait': [WorkflowState.PAGE_LOADED, WorkflowState.CONTENT_ANALYZED, WorkflowState.SELECTOR_AVAILABLE],
68
68
  'solve_captcha': [WorkflowState.PAGE_LOADED, WorkflowState.CONTENT_ANALYZED, WorkflowState.SELECTOR_AVAILABLE],
69
- 'random_scroll': [WorkflowState.PAGE_LOADED, WorkflowState.CONTENT_ANALYZED, WorkflowState.SELECTOR_AVAILABLE]
69
+ 'random_scroll': [WorkflowState.PAGE_LOADED, WorkflowState.CONTENT_ANALYZED, WorkflowState.SELECTOR_AVAILABLE],
70
+ 'save_content_as_markdown': [WorkflowState.CONTENT_ANALYZED, WorkflowState.SELECTOR_AVAILABLE],
71
+ // Allow advanced tools in analyzed state
72
+ 'search_content': [WorkflowState.CONTENT_ANALYZED, WorkflowState.SELECTOR_AVAILABLE],
73
+ 'extract_json': [WorkflowState.CONTENT_ANALYZED, WorkflowState.SELECTOR_AVAILABLE],
74
+ 'scrape_meta_tags': [WorkflowState.PAGE_LOADED, WorkflowState.CONTENT_ANALYZED],
75
+ 'link_harvester': [WorkflowState.PAGE_LOADED, WorkflowState.CONTENT_ANALYZED],
76
+ 'deep_analysis': [WorkflowState.PAGE_LOADED, WorkflowState.CONTENT_ANALYZED]
70
77
  };
71
78
  const allowedStates = toolPrerequisites[toolName];
72
79
  if (!allowedStates) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-browser-mcp-server",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "description": "🦁 MCP server for Brave Real Browser - NPM Workspaces Monorepo with anti-detection features",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,6 +26,8 @@
26
26
  "test:ui": "vitest --ui",
27
27
  "test:coverage": "vitest --coverage",
28
28
  "test:ci": "vitest run --coverage",
29
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
30
+ "test:integration": "npx tsx test/integration/test-all-tools.ts",
29
31
  "publish-all": "node scripts/publish-all.js",
30
32
  "publish-all:dry": "node scripts/publish-all.js --dry-run",
31
33
  "prepare-publish": "node scripts/prepare-publish.js",
@@ -39,7 +41,7 @@
39
41
  "dependencies": {
40
42
  "@modelcontextprotocol/sdk": "latest",
41
43
  "@types/turndown": "latest",
42
- "brave-real-browser": "^2.2.0",
44
+ "brave-real-browser": "^2.3.0",
43
45
  "turndown": "latest"
44
46
  },
45
47
  "peerDependencies": {