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.
- package/dist/browser-manager.js +21 -1
- package/dist/handlers/advanced-tools.js +101 -39
- package/dist/handlers/content-handlers.js +24 -121
- package/dist/handlers/file-handlers.js +2 -11
- package/dist/handlers/navigation-handlers.js +25 -5
- package/dist/workflow-validation.js +8 -1
- package/package.json +4 -2
package/dist/browser-manager.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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 =
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
request.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
url
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
entry
|
|
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
|
-
|
|
534
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
193
|
-
ariaLabel.toLowerCase() === lowerSearchText;
|
|
97
|
+
matches = lowerElementText === lowerSearchText;
|
|
194
98
|
}
|
|
195
99
|
else {
|
|
196
|
-
matches =
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
//
|
|
226
|
-
return elements.
|
|
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
|
-
//
|
|
121
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
44
|
+
"brave-real-browser": "^2.3.0",
|
|
43
45
|
"turndown": "latest"
|
|
44
46
|
},
|
|
45
47
|
"peerDependencies": {
|