brave-real-browser-mcp-server 2.28.1 → 2.29.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.
- package/dist/browser-manager.js +344 -0
- package/dist/handlers/advanced-tools.js +863 -170
- package/dist/handlers/navigation-handlers.js +185 -16
- package/dist/handlers/tool-executor.js +201 -0
- package/dist/tool-definitions.js +104 -6
- package/package.json +2 -2
|
@@ -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(
|
|
27
|
-
//
|
|
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 =
|
|
89
|
+
const maxRetries = retries;
|
|
31
90
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
32
91
|
try {
|
|
33
|
-
tracker.setProgress(
|
|
92
|
+
tracker.setProgress(15 + (attempt - 1) * 15, `🔄 Attempt ${attempt}/${maxRetries}...`);
|
|
34
93
|
await withTimeout(async () => {
|
|
35
|
-
|
|
94
|
+
const navigationOptions = {
|
|
36
95
|
waitUntil: waitUntil,
|
|
37
|
-
timeout:
|
|
38
|
-
}
|
|
39
|
-
|
|
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
|
-
},
|
|
43
|
-
tracker.setProgress(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/tool-definitions.js
CHANGED
|
@@ -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
|
|
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: [
|
|
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.
|
|
3
|
+
"version": "2.29.0",
|
|
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.
|
|
53
|
+
"brave-real-browser": "^2.9.0",
|
|
54
54
|
"puppeteer-core": "^24.35.0",
|
|
55
55
|
"turndown": "latest",
|
|
56
56
|
"vscode-languageserver": "^9.0.1",
|