brave-real-browser-mcp-server 2.28.1 → 2.29.1

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.
@@ -9,7 +9,7 @@ const eventBus = getSharedEventBus();
9
9
  function isApiEndpoint(url) {
10
10
  return /\/api\/|\.json(\?|$)|\?.*=.*&|\/v\d+\//.test(url);
11
11
  }
12
- // Navigation handler with real-time progress
12
+ // Navigation handler with real-time progress and advanced features
13
13
  export async function handleNavigate(args) {
14
14
  const progressNotifier = getProgressNotifier();
15
15
  const progressToken = `navigate-${Date.now()}`;
@@ -22,25 +22,89 @@ export async function handleNavigate(args) {
22
22
  tracker.fail('Browser not initialized');
23
23
  throw new Error('Browser not initialized. Call browser_init first.');
24
24
  }
25
- const { url, waitUntil = 'domcontentloaded' } = args;
26
- tracker.setProgress(10, `📍 Navigating to: ${url}`);
27
- // Navigate with retry logic
25
+ const { url, waitUntil = 'domcontentloaded', blockResources, customHeaders, referrer, waitForSelector, waitForContent, scrollToBottom, randomDelay, bypassCSP, timeout = 60000, retries = 3 } = args;
26
+ tracker.setProgress(5, `📍 Preparing to navigate to: ${url}`);
27
+ // ============================================================
28
+ // STEP 1: Setup request interception for resource blocking
29
+ // ============================================================
30
+ let interceptorEnabled = false;
31
+ const blockedTypes = new Set(blockResources || []);
32
+ if (blockedTypes.size > 0) {
33
+ tracker.setProgress(8, `🚫 Setting up resource blocking: ${Array.from(blockedTypes).join(', ')}`);
34
+ try {
35
+ await pageInstance.setRequestInterception(true);
36
+ interceptorEnabled = true;
37
+ pageInstance.on('request', (request) => {
38
+ const resourceType = request.resourceType();
39
+ if (blockedTypes.has(resourceType)) {
40
+ request.abort();
41
+ }
42
+ else {
43
+ request.continue();
44
+ }
45
+ });
46
+ }
47
+ catch (e) {
48
+ // Interception already enabled or not supported
49
+ }
50
+ }
51
+ // ============================================================
52
+ // STEP 2: Set custom headers if provided
53
+ // ============================================================
54
+ if (customHeaders && Object.keys(customHeaders).length > 0) {
55
+ tracker.setProgress(10, '📝 Setting custom headers...');
56
+ try {
57
+ await pageInstance.setExtraHTTPHeaders(customHeaders);
58
+ }
59
+ catch (e) {
60
+ // Ignore header setting errors
61
+ }
62
+ }
63
+ // ============================================================
64
+ // STEP 3: Bypass CSP if requested
65
+ // ============================================================
66
+ if (bypassCSP) {
67
+ tracker.setProgress(12, '🔓 Bypassing Content Security Policy...');
68
+ try {
69
+ await pageInstance.setBypassCSP(true);
70
+ }
71
+ catch (e) {
72
+ // CSP bypass not supported
73
+ }
74
+ }
75
+ // ============================================================
76
+ // STEP 4: Add random human-like delay
77
+ // ============================================================
78
+ if (randomDelay) {
79
+ const delay = 100 + Math.random() * 400; // 100-500ms
80
+ tracker.setProgress(14, `⏳ Human-like delay: ${Math.round(delay)}ms`);
81
+ await new Promise(r => setTimeout(r, delay));
82
+ }
83
+ tracker.setProgress(15, `📍 Navigating to: ${url}`);
84
+ // ============================================================
85
+ // STEP 5: Navigate with retry logic
86
+ // ============================================================
28
87
  let lastError = null;
29
88
  let success = false;
30
- const maxRetries = 3;
89
+ const maxRetries = retries;
31
90
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
32
91
  try {
33
- tracker.setProgress(20 + (attempt - 1) * 20, `🔄 Attempt ${attempt}/${maxRetries}...`);
92
+ tracker.setProgress(15 + (attempt - 1) * 15, `🔄 Attempt ${attempt}/${maxRetries}...`);
34
93
  await withTimeout(async () => {
35
- await pageInstance.goto(url, {
94
+ const navigationOptions = {
36
95
  waitUntil: waitUntil,
37
- timeout: 60000
38
- });
39
- tracker.setProgress(60, '🛡️ Checking for Cloudflare...');
96
+ timeout: timeout
97
+ };
98
+ // Add referrer if provided
99
+ if (referrer) {
100
+ navigationOptions.referer = referrer;
101
+ }
102
+ await pageInstance.goto(url, navigationOptions);
103
+ tracker.setProgress(50, '🛡️ Checking for Cloudflare...');
40
104
  // Auto-handle Cloudflare challenges if detected
41
105
  await waitForCloudflareBypass(pageInstance, tracker);
42
- }, 90000, 'page-navigation');
43
- tracker.setProgress(80, '✅ Navigation successful');
106
+ }, timeout + 30000, 'page-navigation');
107
+ tracker.setProgress(60, '✅ Navigation successful');
44
108
  success = true;
45
109
  break;
46
110
  }
@@ -62,6 +126,13 @@ export async function handleNavigate(args) {
62
126
  data: text
63
127
  };
64
128
  }, url);
129
+ // Cleanup interception
130
+ if (interceptorEnabled) {
131
+ try {
132
+ await pageInstance.setRequestInterception(false);
133
+ }
134
+ catch { }
135
+ }
65
136
  tracker.complete('📡 API endpoint fetched successfully');
66
137
  return {
67
138
  content: [
@@ -77,19 +148,89 @@ export async function handleNavigate(args) {
77
148
  tracker.setProgress(55, '❌ Fetch fallback failed, retrying navigation...');
78
149
  }
79
150
  }
80
- tracker.setProgress(20 + attempt * 20, `❌ Attempt ${attempt} failed: ${lastError.message}`);
151
+ tracker.setProgress(15 + attempt * 15, `❌ Attempt ${attempt} failed: ${lastError.message}`);
81
152
  if (attempt < maxRetries) {
82
153
  const delay = 1000 * Math.pow(2, attempt - 1);
83
- tracker.setProgress(25 + attempt * 20, `⏳ Retrying in ${delay}ms...`);
154
+ tracker.setProgress(20 + attempt * 15, `⏳ Retrying in ${delay}ms...`);
84
155
  await new Promise(resolve => setTimeout(resolve, delay));
85
156
  }
86
157
  }
87
158
  }
88
159
  if (!success && lastError) {
160
+ // Cleanup interception
161
+ if (interceptorEnabled) {
162
+ try {
163
+ await pageInstance.setRequestInterception(false);
164
+ }
165
+ catch { }
166
+ }
89
167
  tracker.fail(lastError.message);
90
168
  throw lastError;
91
169
  }
92
- tracker.setProgress(90, '📄 Page loaded, preparing response...');
170
+ // ============================================================
171
+ // STEP 6: Wait for specific selector if requested
172
+ // ============================================================
173
+ if (waitForSelector) {
174
+ tracker.setProgress(65, `🔍 Waiting for selector: ${waitForSelector}`);
175
+ try {
176
+ await pageInstance.waitForSelector(waitForSelector, { timeout: 10000, visible: true });
177
+ tracker.setProgress(70, '✅ Selector found');
178
+ }
179
+ catch (e) {
180
+ tracker.setProgress(70, `⚠️ Selector not found: ${waitForSelector}`);
181
+ }
182
+ }
183
+ // ============================================================
184
+ // STEP 7: Wait for specific content if requested
185
+ // ============================================================
186
+ if (waitForContent) {
187
+ tracker.setProgress(72, `📝 Waiting for content: "${waitForContent.substring(0, 30)}..."`);
188
+ try {
189
+ await pageInstance.waitForFunction((text) => document.body?.innerText?.includes(text), { timeout: 10000 }, waitForContent);
190
+ tracker.setProgress(75, '✅ Content found');
191
+ }
192
+ catch (e) {
193
+ tracker.setProgress(75, `⚠️ Content not found: "${waitForContent.substring(0, 30)}..."`);
194
+ }
195
+ }
196
+ // ============================================================
197
+ // STEP 8: Auto-scroll to trigger lazy loading
198
+ // ============================================================
199
+ if (scrollToBottom) {
200
+ tracker.setProgress(77, '📜 Auto-scrolling to trigger lazy loading...');
201
+ try {
202
+ await pageInstance.evaluate(async () => {
203
+ const scrollStep = window.innerHeight;
204
+ const scrollDelay = 200;
205
+ let totalScrolled = 0;
206
+ const maxScroll = Math.min(document.body.scrollHeight, 10000);
207
+ while (totalScrolled < maxScroll) {
208
+ window.scrollBy(0, scrollStep);
209
+ totalScrolled += scrollStep;
210
+ await new Promise(r => setTimeout(r, scrollDelay));
211
+ }
212
+ // Scroll back to top
213
+ window.scrollTo(0, 0);
214
+ });
215
+ // Wait for lazy content to load
216
+ await new Promise(r => setTimeout(r, 1000));
217
+ tracker.setProgress(82, '✅ Auto-scroll completed');
218
+ }
219
+ catch (e) {
220
+ tracker.setProgress(82, '⚠️ Auto-scroll failed');
221
+ }
222
+ }
223
+ // ============================================================
224
+ // STEP 9: Cleanup and prepare response
225
+ // ============================================================
226
+ // Disable request interception if it was enabled
227
+ if (interceptorEnabled) {
228
+ try {
229
+ await pageInstance.setRequestInterception(false);
230
+ }
231
+ catch { }
232
+ }
233
+ tracker.setProgress(85, '📄 Page loaded, preparing response...');
93
234
  // Publish browser state update to event bus
94
235
  try {
95
236
  eventBus.publish({
@@ -106,6 +247,34 @@ export async function handleNavigate(args) {
106
247
  catch (error) {
107
248
  // Ignore event bus errors
108
249
  }
250
+ // Get page info for response
251
+ let pageInfo = '';
252
+ try {
253
+ const title = await pageInstance.title();
254
+ const finalUrl = pageInstance.url();
255
+ pageInfo = `\n📄 Title: ${title}\n🔗 Final URL: ${finalUrl}`;
256
+ }
257
+ catch { }
258
+ const advancedOptionsUsed = [];
259
+ if (blockResources?.length)
260
+ advancedOptionsUsed.push(`Blocked: ${blockResources.join(', ')}`);
261
+ if (customHeaders)
262
+ advancedOptionsUsed.push('Custom headers set');
263
+ if (referrer)
264
+ advancedOptionsUsed.push(`Referrer: ${referrer}`);
265
+ if (waitForSelector)
266
+ advancedOptionsUsed.push(`Waited for: ${waitForSelector}`);
267
+ if (waitForContent)
268
+ advancedOptionsUsed.push(`Waited for content`);
269
+ if (scrollToBottom)
270
+ advancedOptionsUsed.push('Auto-scrolled');
271
+ if (randomDelay)
272
+ advancedOptionsUsed.push('Human delay added');
273
+ if (bypassCSP)
274
+ advancedOptionsUsed.push('CSP bypassed');
275
+ const advancedInfo = advancedOptionsUsed.length > 0
276
+ ? `\n\n⚡ Advanced options: ${advancedOptionsUsed.join(' | ')}`
277
+ : '';
109
278
  const workflowMessage = '\n\n🔄 Workflow Status: Page loaded\n' +
110
279
  ' • Next step: Use get_content to analyze page content\n' +
111
280
  ' • Then: Use find_selector to locate elements\n' +
@@ -116,7 +285,7 @@ export async function handleNavigate(args) {
116
285
  content: [
117
286
  {
118
287
  type: 'text',
119
- text: `Successfully navigated to ${url}${workflowMessage}`,
288
+ text: `Successfully navigated to ${url}${pageInfo}${advancedInfo}${workflowMessage}`,
120
289
  },
121
290
  ],
122
291
  };
@@ -247,3 +247,204 @@ export async function executeBatchWithProgress(toolName, items, operation) {
247
247
  return results;
248
248
  }, { totalSteps: items.length + 2 });
249
249
  }
250
+ // Default recovery strategies for common browser automation errors
251
+ const defaultRecoveryStrategies = [
252
+ // Network errors - retry with delay
253
+ { errorPattern: /net::ERR_|ECONNREFUSED|ETIMEDOUT|socket hang up/i, action: 'retry', maxRetries: 3, delay: 2000 },
254
+ // Navigation errors - refresh and retry
255
+ { errorPattern: /Navigation timeout|navigation.*timed out/i, action: 'refresh', maxRetries: 2, delay: 1000 },
256
+ // Frame/Context errors - restart browser
257
+ { errorPattern: /frame was detached|Execution context was destroyed|Target closed/i, action: 'restart_browser', maxRetries: 1 },
258
+ // Session errors - restart browser
259
+ { errorPattern: /Session closed|Protocol error|Session timed out/i, action: 'restart_browser', maxRetries: 1 },
260
+ // Element not found - retry with delay
261
+ { errorPattern: /Element.*not found|waiting for selector|failed to find/i, action: 'retry', maxRetries: 3, delay: 1000 },
262
+ // Rate limiting - wait and retry
263
+ { errorPattern: /rate limit|too many requests|429/i, action: 'retry', maxRetries: 3, delay: 5000 },
264
+ // Cloudflare/Bot detection - skip (handled separately)
265
+ { errorPattern: /cloudflare|captcha|bot detected|access denied/i, action: 'skip', maxRetries: 0 },
266
+ ];
267
+ // Global error recovery configuration
268
+ let errorRecoveryConfig = {
269
+ enabled: true,
270
+ maxGlobalRetries: 5,
271
+ defaultDelay: 1000,
272
+ strategies: defaultRecoveryStrategies
273
+ };
274
+ /**
275
+ * Configure error recovery
276
+ */
277
+ export function configureErrorRecovery(config) {
278
+ errorRecoveryConfig = { ...errorRecoveryConfig, ...config };
279
+ if (config.strategies) {
280
+ errorRecoveryConfig.strategies = [...config.strategies, ...defaultRecoveryStrategies];
281
+ }
282
+ }
283
+ /**
284
+ * Add a custom recovery strategy
285
+ */
286
+ export function addRecoveryStrategy(strategy) {
287
+ errorRecoveryConfig.strategies.unshift(strategy); // Add at beginning for priority
288
+ }
289
+ /**
290
+ * Get matching recovery strategy for an error
291
+ */
292
+ function getRecoveryStrategy(error) {
293
+ const errorMessage = error.message || String(error);
294
+ for (const strategy of errorRecoveryConfig.strategies) {
295
+ const pattern = typeof strategy.errorPattern === 'string'
296
+ ? new RegExp(strategy.errorPattern, 'i')
297
+ : strategy.errorPattern;
298
+ if (pattern.test(errorMessage)) {
299
+ return strategy;
300
+ }
301
+ }
302
+ return null;
303
+ }
304
+ const recoveryState = {
305
+ globalRetries: 0,
306
+ errorCounts: new Map(),
307
+ lastRecoveryTime: 0
308
+ };
309
+ /**
310
+ * Reset recovery state (call after successful operation)
311
+ */
312
+ export function resetRecoveryState() {
313
+ recoveryState.globalRetries = 0;
314
+ recoveryState.errorCounts.clear();
315
+ }
316
+ /**
317
+ * Execute with auto-recovery
318
+ */
319
+ export async function executeWithRecovery(operation, options) {
320
+ if (!errorRecoveryConfig.enabled) {
321
+ try {
322
+ const result = await operation();
323
+ return { success: true, result, recoveryAttempts: 0 };
324
+ }
325
+ catch (error) {
326
+ return { success: false, error: error, recoveryAttempts: 0 };
327
+ }
328
+ }
329
+ let lastError = null;
330
+ let attempts = 0;
331
+ while (recoveryState.globalRetries < errorRecoveryConfig.maxGlobalRetries) {
332
+ try {
333
+ const result = await operation();
334
+ // Success - reset relevant counters
335
+ if (attempts > 0) {
336
+ options.onProgress?.(`✅ Recovery successful after ${attempts} attempts`);
337
+ }
338
+ return { success: true, result, recoveryAttempts: attempts };
339
+ }
340
+ catch (error) {
341
+ lastError = error;
342
+ const strategy = getRecoveryStrategy(lastError);
343
+ if (!strategy) {
344
+ // No recovery strategy for this error
345
+ options.onProgress?.(`❌ No recovery strategy for: ${lastError.message}`);
346
+ return { success: false, error: lastError, recoveryAttempts: attempts };
347
+ }
348
+ // Check error-specific retry count
349
+ const errorKey = strategy.errorPattern.toString();
350
+ const errorCount = recoveryState.errorCounts.get(errorKey) || 0;
351
+ if (errorCount >= (strategy.maxRetries || 3)) {
352
+ options.onProgress?.(`❌ Max retries (${strategy.maxRetries}) reached for: ${errorKey}`);
353
+ return { success: false, error: lastError, recoveryAttempts: attempts };
354
+ }
355
+ // Increment counters
356
+ recoveryState.globalRetries++;
357
+ recoveryState.errorCounts.set(errorKey, errorCount + 1);
358
+ attempts++;
359
+ // Execute recovery action
360
+ const delay = strategy.delay || errorRecoveryConfig.defaultDelay;
361
+ options.onProgress?.(`🔄 Recovery attempt ${attempts}: ${strategy.action} (${lastError.message.substring(0, 50)}...)`);
362
+ // Notify callback
363
+ errorRecoveryConfig.onRecovery?.(lastError, strategy.action, attempts);
364
+ switch (strategy.action) {
365
+ case 'retry':
366
+ // Simple retry after delay
367
+ await new Promise(r => setTimeout(r, delay));
368
+ break;
369
+ case 'refresh':
370
+ // Refresh the page and retry
371
+ if (options.page) {
372
+ try {
373
+ await options.page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 });
374
+ await new Promise(r => setTimeout(r, delay));
375
+ }
376
+ catch (e) {
377
+ // Refresh failed, continue anyway
378
+ }
379
+ }
380
+ break;
381
+ case 'restart_browser':
382
+ // Restart the browser
383
+ if (options.restartBrowser) {
384
+ try {
385
+ options.onProgress?.('🔄 Restarting browser...');
386
+ await options.restartBrowser();
387
+ await new Promise(r => setTimeout(r, delay));
388
+ }
389
+ catch (e) {
390
+ options.onProgress?.(`⚠️ Browser restart failed: ${e.message}`);
391
+ }
392
+ }
393
+ break;
394
+ case 'fallback':
395
+ // Try fallback function
396
+ if (strategy.fallbackFn) {
397
+ try {
398
+ const fallbackResult = await strategy.fallbackFn();
399
+ return { success: true, result: fallbackResult, recoveryAttempts: attempts };
400
+ }
401
+ catch (e) {
402
+ // Fallback failed, continue with retry
403
+ }
404
+ }
405
+ break;
406
+ case 'skip':
407
+ // Skip this operation entirely
408
+ options.onProgress?.(`⏭️ Skipping operation due to: ${lastError.message}`);
409
+ return { success: false, error: lastError, recoveryAttempts: attempts };
410
+ }
411
+ recoveryState.lastRecoveryTime = Date.now();
412
+ }
413
+ }
414
+ // Max global retries reached
415
+ options.onProgress?.(`❌ Max global retries (${errorRecoveryConfig.maxGlobalRetries}) reached`);
416
+ return { success: false, error: lastError || new Error('Max retries reached'), recoveryAttempts: attempts };
417
+ }
418
+ /**
419
+ * Decorator to add auto-recovery to any async function
420
+ */
421
+ export function withAutoRecovery(toolName, options = {}) {
422
+ return function (handler) {
423
+ return async function (...args) {
424
+ // Check if first arg is a page object
425
+ const page = args[0]?.goto ? args[0] : undefined;
426
+ const result = await executeWithRecovery(() => handler(...args), {
427
+ toolName,
428
+ page,
429
+ onProgress: (msg) => console.log(`[${toolName}] ${msg}`)
430
+ });
431
+ if (!result.success) {
432
+ throw result.error;
433
+ }
434
+ return result.result;
435
+ };
436
+ };
437
+ }
438
+ /**
439
+ * Get current recovery statistics
440
+ */
441
+ export function getRecoveryStats() {
442
+ return {
443
+ enabled: errorRecoveryConfig.enabled,
444
+ globalRetries: recoveryState.globalRetries,
445
+ maxGlobalRetries: errorRecoveryConfig.maxGlobalRetries,
446
+ errorCounts: Object.fromEntries(recoveryState.errorCounts),
447
+ lastRecoveryTime: recoveryState.lastRecoveryTime,
448
+ strategiesCount: errorRecoveryConfig.strategies.length
449
+ };
450
+ }
@@ -195,7 +195,7 @@ export const TOOLS = [
195
195
  },
196
196
  {
197
197
  name: 'navigate',
198
- description: 'Navigate to a URL',
198
+ description: 'Navigate to a URL with advanced options: resource blocking, custom headers, auto-scroll, wait for content, and anti-detection features.',
199
199
  inputSchema: {
200
200
  type: 'object',
201
201
  additionalProperties: false,
@@ -210,6 +210,53 @@ export const TOOLS = [
210
210
  enum: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'],
211
211
  default: 'domcontentloaded',
212
212
  },
213
+ // Advanced options
214
+ blockResources: {
215
+ type: 'array',
216
+ items: { type: 'string', enum: ['image', 'stylesheet', 'font', 'media', 'script'] },
217
+ description: 'Block resource types for faster loading (e.g., ["image", "font"])',
218
+ },
219
+ customHeaders: {
220
+ type: 'object',
221
+ description: 'Custom headers to send with request (e.g., {"Authorization": "Bearer xxx"})',
222
+ },
223
+ referrer: {
224
+ type: 'string',
225
+ description: 'Custom referrer URL',
226
+ },
227
+ waitForSelector: {
228
+ type: 'string',
229
+ description: 'Wait for specific CSS selector after navigation',
230
+ },
231
+ waitForContent: {
232
+ type: 'string',
233
+ description: 'Wait for specific text content to appear',
234
+ },
235
+ scrollToBottom: {
236
+ type: 'boolean',
237
+ description: 'Auto-scroll to bottom to trigger lazy loading',
238
+ default: false,
239
+ },
240
+ randomDelay: {
241
+ type: 'boolean',
242
+ description: 'Add random human-like delay (100-500ms) before navigation',
243
+ default: false,
244
+ },
245
+ bypassCSP: {
246
+ type: 'boolean',
247
+ description: 'Bypass Content Security Policy',
248
+ default: false,
249
+ },
250
+ timeout: {
251
+ type: 'number',
252
+ description: 'Navigation timeout in ms',
253
+ default: 60000,
254
+ },
255
+ retries: {
256
+ type: 'number',
257
+ description: 'Number of retry attempts on failure',
258
+ default: 3,
259
+ },
213
260
  },
214
261
  required: ['url'],
215
262
  },
@@ -648,7 +695,7 @@ export const TOOLS = [
648
695
  },
649
696
  {
650
697
  name: 'network_recorder',
651
- description: 'Record full network traffic including headers and body. Also discovers hidden API endpoints.',
698
+ description: 'Record full network traffic including headers and body. Also discovers hidden API endpoints. NEW: API interception with request blocking, mocking, and header modification.',
652
699
  inputSchema: {
653
700
  type: 'object',
654
701
  additionalProperties: false,
@@ -660,6 +707,33 @@ export const TOOLS = [
660
707
  // Merged from api_finder
661
708
  findApis: { type: 'boolean', description: 'Discover hidden API endpoints (XHR/fetch)', default: false },
662
709
  apiPatterns: { type: 'array', items: { type: 'string' }, description: 'URL patterns to match for API discovery' },
710
+ // API Interceptor options
711
+ interceptMode: { type: 'string', enum: ['record', 'intercept', 'mock'], description: 'Mode: record (passive), intercept (active with blocking/modifying), mock (respond with fake data)', default: 'record' },
712
+ blockPatterns: { type: 'array', items: { type: 'string' }, description: 'URL patterns to block (e.g., ["ads", "/tracking/"])' },
713
+ mockResponses: {
714
+ type: 'array',
715
+ items: {
716
+ type: 'object',
717
+ properties: {
718
+ urlPattern: { type: 'string' },
719
+ response: { type: 'object' },
720
+ statusCode: { type: 'number' }
721
+ }
722
+ },
723
+ description: 'Mock responses for matching URLs'
724
+ },
725
+ modifyHeaders: {
726
+ type: 'array',
727
+ items: {
728
+ type: 'object',
729
+ properties: {
730
+ urlPattern: { type: 'string' },
731
+ headers: { type: 'object' }
732
+ }
733
+ },
734
+ description: 'Modify headers for matching URLs'
735
+ },
736
+ capturePayloads: { type: 'boolean', description: 'Capture POST/PUT request bodies', default: false },
663
737
  },
664
738
  },
665
739
  },
@@ -670,7 +744,7 @@ export const TOOLS = [
670
744
  // batch_element_scraper REMOVED - merged into find_element (use batchMode: true)
671
745
  {
672
746
  name: 'link_harvester',
673
- description: 'Harvest all links from page with filtering options',
747
+ description: 'Harvest all links from page with filtering options. NEW: Auto-follow pagination to scrape multiple pages automatically.',
674
748
  inputSchema: {
675
749
  type: 'object',
676
750
  additionalProperties: false,
@@ -679,6 +753,11 @@ export const TOOLS = [
679
753
  includeExternal: { type: 'boolean', description: 'Include external links', default: true },
680
754
  includeInternal: { type: 'boolean', description: 'Include internal links', default: true },
681
755
  maxLinks: { type: 'number', description: 'Maximum links to return' },
756
+ // Pagination options
757
+ followPagination: { type: 'boolean', description: 'Auto-follow pagination links to scrape multiple pages', default: false },
758
+ maxPages: { type: 'number', description: 'Maximum pages to scrape when following pagination (1-20)', default: 5 },
759
+ paginationSelector: { type: 'string', description: 'Custom CSS selector for the next page link (e.g., "a.next-page")' },
760
+ delayBetweenPages: { type: 'number', description: 'Delay between page navigations in ms', default: 1000 },
682
761
  },
683
762
  },
684
763
  },
@@ -746,7 +825,7 @@ export const TOOLS = [
746
825
  },
747
826
  {
748
827
  name: 'stream_extractor',
749
- description: 'Master tool: Extract direct download/stream URLs from any page with automatic redirect following, countdown waiting, and format detection',
828
+ description: 'Master tool: Extract direct download/stream URLs from any page with automatic redirect following, countdown waiting, format detection, multi-quality selection, and VidSrc/Filemoon/StreamWish support',
750
829
  inputSchema: {
751
830
  type: 'object',
752
831
  additionalProperties: false,
@@ -762,17 +841,32 @@ export const TOOLS = [
762
841
  default: ['mp4', 'mkv', 'm3u8', 'mp3'],
763
842
  },
764
843
  quality: { type: 'string', description: 'Preferred quality (highest, lowest, 1080p, 720p)', default: 'highest' },
844
+ autoSelectBest: { type: 'boolean', description: 'Auto-select highest quality stream', default: true },
845
+ preferredQuality: {
846
+ type: 'string',
847
+ enum: ['1080p', '720p', '480p', '360p', 'highest', 'lowest'],
848
+ description: 'Preferred quality to select',
849
+ default: 'highest'
850
+ },
851
+ extractSubtitles: { type: 'boolean', description: 'Also extract VTT/SRT subtitles', default: false },
852
+ siteType: {
853
+ type: 'string',
854
+ enum: ['auto', 'vidsrc', 'filemoon', 'streamwish', 'doodstream', 'mixdrop', 'streamtape', 'vidcloud', 'mp4upload'],
855
+ description: 'Site-specific extraction mode (auto-detects if not specified)',
856
+ default: 'auto'
857
+ },
765
858
  },
766
859
  },
767
860
  },
768
861
  {
769
862
  name: 'js_scrape',
770
- description: 'Single-call JavaScript-rendered content extraction. Combines navigation, auto-wait, scrolling, and content extraction. Perfect for AJAX/dynamic pages that Jsoup cannot parse.',
863
+ description: 'Single-call JavaScript-rendered content extraction. Combines navigation, auto-wait, scrolling, and content extraction. Perfect for AJAX/dynamic pages that Jsoup cannot parse. NEW: Supports parallel scraping of multiple URLs with concurrency control.',
771
864
  inputSchema: {
772
865
  type: 'object',
773
866
  additionalProperties: false,
774
867
  properties: {
775
868
  url: { type: 'string', description: 'URL to scrape (required)' },
869
+ urls: { type: 'array', items: { type: 'string' }, description: 'Multiple URLs for parallel scraping (alternative to url)' },
776
870
  waitForSelector: { type: 'string', description: 'CSS selector to wait for before extracting content' },
777
871
  waitForTimeout: { type: 'number', description: 'Maximum wait time in ms', default: 10000 },
778
872
  extractSelector: { type: 'string', description: 'CSS selector for specific elements to extract (optional, extracts full page if not specified)' },
@@ -780,8 +874,12 @@ export const TOOLS = [
780
874
  returnType: { type: 'string', enum: ['html', 'text', 'elements'], description: 'Return format', default: 'html' },
781
875
  scrollToLoad: { type: 'boolean', description: 'Scroll page to trigger lazy loading', default: true },
782
876
  closeBrowserAfter: { type: 'boolean', description: 'Close browser after scraping', default: false },
877
+ // Parallel scraping options
878
+ concurrency: { type: 'number', description: 'Max concurrent scrapes for parallel mode (1-10)', default: 3 },
879
+ continueOnError: { type: 'boolean', description: 'Continue scraping even if some URLs fail', default: true },
880
+ delayBetween: { type: 'number', description: 'Delay between starting each scrape in ms', default: 500 },
783
881
  },
784
- required: ['url'],
882
+ required: [],
785
883
  },
786
884
  },
787
885
  // ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-browser-mcp-server",
3
- "version": "2.28.1",
3
+ "version": "2.29.1",
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.8.33",
53
+ "brave-real-browser": "^2.9.1",
54
54
  "puppeteer-core": "^24.35.0",
55
55
  "turndown": "latest",
56
56
  "vscode-languageserver": "^9.0.1",