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.
package/README.md CHANGED
@@ -107,6 +107,115 @@ npm run dev
107
107
 
108
108
  ---
109
109
 
110
+ ## ๐Ÿ†• New in v2.28.1 - Advanced Enhancements
111
+
112
+ ### ๐ŸŒ Advanced Navigation
113
+ | Option | Description |
114
+ |--------|-------------|
115
+ | `blockResources` | Block images, fonts, CSS for faster loading |
116
+ | `customHeaders` | Set custom HTTP headers |
117
+ | `referrer` | Custom referrer URL |
118
+ | `waitForSelector` | Wait for specific element after load |
119
+ | `waitForContent` | Wait for specific text content |
120
+ | `scrollToBottom` | Auto-scroll for lazy loading |
121
+ | `randomDelay` | Human-like delay (100-500ms) |
122
+ | `bypassCSP` | Bypass Content Security Policy |
123
+
124
+ **Example:**
125
+ ```json
126
+ {
127
+ "url": "https://example.com",
128
+ "blockResources": ["image", "font"],
129
+ "waitForSelector": ".main-content",
130
+ "scrollToBottom": true,
131
+ "randomDelay": true
132
+ }
133
+ ```
134
+
135
+ ### ๐Ÿš€ Parallel Scraping (js_scrape)
136
+ | Option | Description |
137
+ |--------|-------------|
138
+ | `urls` | Array of multiple URLs to scrape |
139
+ | `concurrency` | Max concurrent scrapes (1-10) |
140
+ | `continueOnError` | Continue even if some URLs fail |
141
+ | `delayBetween` | Delay between scrapes (ms) |
142
+
143
+ **Example:**
144
+ ```json
145
+ {
146
+ "urls": ["https://site1.com", "https://site2.com", "https://site3.com"],
147
+ "concurrency": 3,
148
+ "extractSelector": "article"
149
+ }
150
+ ```
151
+
152
+ ### ๐Ÿ“„ Auto-Pagination (link_harvester)
153
+ | Option | Description |
154
+ |--------|-------------|
155
+ | `followPagination` | Auto-follow pagination links |
156
+ | `maxPages` | Maximum pages to scrape (1-20) |
157
+ | `paginationSelector` | Custom next page selector |
158
+ | `delayBetweenPages` | Delay between pages (ms) |
159
+
160
+ **Example:**
161
+ ```json
162
+ {
163
+ "followPagination": true,
164
+ "maxPages": 10,
165
+ "filter": "movie"
166
+ }
167
+ ```
168
+
169
+ ### ๐Ÿ”Œ API Interceptor (network_recorder)
170
+ | Option | Description |
171
+ |--------|-------------|
172
+ | `interceptMode` | `record`, `intercept`, or `mock` |
173
+ | `blockPatterns` | URL patterns to block |
174
+ | `mockResponses` | Fake responses for URLs |
175
+ | `modifyHeaders` | Modify request headers |
176
+ | `capturePayloads` | Capture POST/PUT bodies |
177
+
178
+ **Example:**
179
+ ```json
180
+ {
181
+ "interceptMode": "intercept",
182
+ "blockPatterns": ["ads", "tracking"],
183
+ "capturePayloads": true
184
+ }
185
+ ```
186
+
187
+ ### ๐Ÿ›ก๏ธ Anti-Detection Enhancements
188
+
189
+ | Feature | Description |
190
+ |---------|-------------|
191
+ | **Fingerprint Randomizer** | Canvas, Audio, Hardware randomization |
192
+ | **Human Behavior Simulation** | Natural mouse/scroll patterns |
193
+ | **WebGL Spoofing** | Random GPU from 8 configs (Intel, NVIDIA, AMD) |
194
+ | **Proxy Rotation** | round-robin, random, least-used, on-error strategies |
195
+ | **Rate Limiter** | Per-second/minute limits, domain-specific |
196
+ | **Error Auto-Recovery** | Smart retry with different strategies |
197
+
198
+ ### ๐Ÿ”„ Internal Systems
199
+
200
+ ```javascript
201
+ // Rate Limiter
202
+ initRateLimiter({ requestsPerSecond: 5, requestsPerMinute: 100 });
203
+ setDomainRateLimit('api.example.com', 2); // 2 req/sec for this domain
204
+
205
+ // Proxy Rotation
206
+ initProxyRotation(['proxy1:8080', 'proxy2:8080'], 'round-robin');
207
+ rotateProxy('error'); // Rotate on error
208
+
209
+ // Error Recovery
210
+ executeWithRecovery(operation, {
211
+ toolName: 'navigate',
212
+ page: pageInstance,
213
+ restartBrowser: () => browser.restart()
214
+ });
215
+ ```
216
+
217
+ ---
218
+
110
219
  ## ๐ŸŒ Unified MCP+LSP+SSE Ecosystem
111
220
 
112
221
  **เคเค• command เคธเฅ‡ เคธเคฌ active:**
@@ -117,7 +226,7 @@ npm run dev
117
226
 
118
227
  ### Output:
119
228
  ```
120
- ๐Ÿฆ Brave Real Browser - Unified Server v2.22.0
229
+ ๐Ÿฆ Brave Real Browser - Unified Server v2.28.1
121
230
  โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
122
231
  ๐Ÿ“ก HTTP Server: http://localhost:3000
123
232
 
@@ -185,14 +294,14 @@ brave-real-launcher
185
294
  ### Navigation
186
295
  | Tool | Description |
187
296
  |------|-------------|
188
- | `navigate` | Navigate to URL |
297
+ | `navigate` | Navigate to URL with advanced options (resource blocking, custom headers, auto-scroll) |
189
298
  | `wait` | Wait for conditions |
190
299
 
191
300
  ### Content
192
301
  | Tool | Description |
193
302
  |------|-------------|
194
303
  | `get_content` | Get page HTML/text |
195
- | `find_element` | Find by selector/text/AI |
304
+ | `find_element` | Find by selector/text/AI with batch mode |
196
305
  | `save_content_as_markdown` | Save as markdown file |
197
306
 
198
307
  ### Interaction
@@ -204,34 +313,32 @@ brave-real-launcher
204
313
  | `random_scroll` | Natural scrolling |
205
314
  | `solve_captcha` | Solve CAPTCHAs |
206
315
 
207
- ### Media Extraction
316
+ ### Media & Streaming
317
+ | Tool | Description |
318
+ |------|-------------|
319
+ | `stream_extractor` | Extract streams with multi-quality selector, VidSrc/Filemoon/StreamWish support |
320
+ | `player_api_hook` | Hook into JWPlayer, Video.js, HLS.js, Plyr, Vidstack, DASH.js |
321
+ | `iframe_handler` | Deep iframe scraping with video source extraction |
322
+
323
+ ### Scraping
208
324
  | Tool | Description |
209
325
  |------|-------------|
210
- | `media_extractor` | Extract video/audio |
211
- | `m3u8_parser` | Parse HLS streams |
212
- | `stream_extractor` | Direct download URLs |
326
+ | `js_scrape` | JavaScript-rendered scraping with parallel URL support |
327
+ | `link_harvester` | Harvest links with auto-pagination |
328
+ | `network_recorder` | Record traffic with API interception |
329
+ | `extract_json` | Extract embedded JSON with AES decryption |
330
+ | `search_regex` | Regex search like regex101.com |
213
331
 
214
332
  ### Advanced
215
333
  | Tool | Description |
216
334
  |------|-------------|
217
- | `search_content` | Search patterns |
218
- | `extract_json` | Extract embedded JSON |
219
335
  | `scrape_meta_tags` | Meta/OG tags |
220
- | `deep_analysis` | Full page analysis |
221
- | `network_recorder` | Record traffic |
222
- | `api_finder` | Discover APIs |
223
- | `ajax_content_waiter` | Wait for AJAX |
224
- | `link_harvester` | Harvest links |
225
- | `batch_element_scraper` | Batch scrape |
226
- | `extract_schema` | Schema.org data |
227
- | `element_screenshot` | Screenshot element |
228
- | `breadcrumb_navigator` | Navigate breadcrumbs |
336
+ | `deep_analysis` | Full page analysis with screenshot |
229
337
  | `redirect_tracer` | Trace redirects |
230
338
  | `progress_tracker` | Track progress |
231
339
  | `cookie_manager` | Manage cookies |
232
340
  | `file_downloader` | Download files |
233
- | `iframe_handler` | Handle iframes |
234
- | `popup_handler` | Handle popups |
341
+ | `execute_js` | Run custom JavaScript |
235
342
 
236
343
  ---
237
344
 
@@ -36,6 +36,350 @@ function loadEnvFile() {
36
36
  }
37
37
  }
38
38
  loadEnvFile();
39
+ // Global proxy rotation state
40
+ let proxyRotation = {
41
+ enabled: false,
42
+ proxies: [],
43
+ rotationStrategy: 'round-robin',
44
+ rotateAfterRequests: 50,
45
+ rotateOnBlock: true,
46
+ currentIndex: 0,
47
+ requestCount: 0,
48
+ proxyStats: new Map()
49
+ };
50
+ // Initialize proxy rotation
51
+ export function initProxyRotation(proxies, strategy = 'round-robin') {
52
+ const parsedProxies = proxies.map(p => {
53
+ if (typeof p === 'string') {
54
+ // Parse string format: protocol://user:pass@host:port or host:port
55
+ const match = p.match(/^(?:(https?|socks5):\/\/)?(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/);
56
+ if (match) {
57
+ return {
58
+ protocol: match[1] || 'http',
59
+ username: match[2],
60
+ password: match[3],
61
+ host: match[4],
62
+ port: parseInt(match[5], 10)
63
+ };
64
+ }
65
+ // Simple host:port
66
+ const [host, port] = p.split(':');
67
+ return { host, port: parseInt(port, 10), protocol: 'http' };
68
+ }
69
+ return p;
70
+ });
71
+ proxyRotation = {
72
+ enabled: true,
73
+ proxies: parsedProxies,
74
+ rotationStrategy: strategy,
75
+ rotateAfterRequests: 50,
76
+ rotateOnBlock: true,
77
+ currentIndex: 0,
78
+ requestCount: 0,
79
+ proxyStats: new Map()
80
+ };
81
+ // Initialize stats for each proxy
82
+ parsedProxies.forEach(p => {
83
+ const key = `${p.host}:${p.port}`;
84
+ proxyRotation.proxyStats.set(key, { uses: 0, failures: 0, lastUsed: 0 });
85
+ });
86
+ console.log(`[ProxyRotation] Initialized with ${parsedProxies.length} proxies, strategy: ${strategy}`);
87
+ }
88
+ // Get current proxy
89
+ export function getCurrentProxy() {
90
+ if (!proxyRotation.enabled || proxyRotation.proxies.length === 0) {
91
+ return null;
92
+ }
93
+ return proxyRotation.proxies[proxyRotation.currentIndex];
94
+ }
95
+ // Get proxy as URL string
96
+ export function getCurrentProxyUrl() {
97
+ const proxy = getCurrentProxy();
98
+ if (!proxy)
99
+ return null;
100
+ let url = `${proxy.protocol || 'http'}://`;
101
+ if (proxy.username && proxy.password) {
102
+ url += `${proxy.username}:${proxy.password}@`;
103
+ }
104
+ url += `${proxy.host}:${proxy.port}`;
105
+ return url;
106
+ }
107
+ // Rotate to next proxy
108
+ export function rotateProxy(reason = 'scheduled') {
109
+ if (!proxyRotation.enabled || proxyRotation.proxies.length === 0) {
110
+ return null;
111
+ }
112
+ const prevProxy = getCurrentProxy();
113
+ const prevKey = prevProxy ? `${prevProxy.host}:${prevProxy.port}` : '';
114
+ switch (proxyRotation.rotationStrategy) {
115
+ case 'round-robin':
116
+ proxyRotation.currentIndex = (proxyRotation.currentIndex + 1) % proxyRotation.proxies.length;
117
+ break;
118
+ case 'random':
119
+ proxyRotation.currentIndex = Math.floor(Math.random() * proxyRotation.proxies.length);
120
+ break;
121
+ case 'least-used':
122
+ let minUses = Infinity;
123
+ let minIndex = 0;
124
+ proxyRotation.proxies.forEach((p, i) => {
125
+ const key = `${p.host}:${p.port}`;
126
+ const stats = proxyRotation.proxyStats.get(key);
127
+ if (stats && stats.uses < minUses) {
128
+ minUses = stats.uses;
129
+ minIndex = i;
130
+ }
131
+ });
132
+ proxyRotation.currentIndex = minIndex;
133
+ break;
134
+ case 'on-error':
135
+ // Only rotate on error/blocked, otherwise keep current
136
+ if (reason === 'error' || reason === 'blocked') {
137
+ proxyRotation.currentIndex = (proxyRotation.currentIndex + 1) % proxyRotation.proxies.length;
138
+ }
139
+ break;
140
+ }
141
+ proxyRotation.requestCount = 0;
142
+ const newProxy = getCurrentProxy();
143
+ const newKey = newProxy ? `${newProxy.host}:${newProxy.port}` : '';
144
+ // Update stats
145
+ if (newKey && proxyRotation.proxyStats.has(newKey)) {
146
+ const stats = proxyRotation.proxyStats.get(newKey);
147
+ stats.uses++;
148
+ stats.lastUsed = Date.now();
149
+ if (reason === 'error' || reason === 'blocked') {
150
+ const prevStats = proxyRotation.proxyStats.get(prevKey);
151
+ if (prevStats)
152
+ prevStats.failures++;
153
+ }
154
+ }
155
+ console.log(`[ProxyRotation] Rotated proxy (${reason}): ${prevKey} -> ${newKey}`);
156
+ return newProxy;
157
+ }
158
+ // Check if proxy should be rotated
159
+ export function shouldRotateProxy() {
160
+ if (!proxyRotation.enabled)
161
+ return false;
162
+ proxyRotation.requestCount++;
163
+ if (proxyRotation.rotateAfterRequests &&
164
+ proxyRotation.requestCount >= proxyRotation.rotateAfterRequests) {
165
+ return true;
166
+ }
167
+ return false;
168
+ }
169
+ // Mark current proxy as blocked/failed
170
+ export function markProxyFailed(blocked = false) {
171
+ if (!proxyRotation.enabled)
172
+ return;
173
+ const proxy = getCurrentProxy();
174
+ if (proxy) {
175
+ const key = `${proxy.host}:${proxy.port}`;
176
+ const stats = proxyRotation.proxyStats.get(key);
177
+ if (stats) {
178
+ stats.failures++;
179
+ }
180
+ }
181
+ if (proxyRotation.rotateOnBlock && blocked) {
182
+ rotateProxy('blocked');
183
+ }
184
+ }
185
+ // Get proxy stats
186
+ export function getProxyStats() {
187
+ return {
188
+ enabled: proxyRotation.enabled,
189
+ currentProxy: getCurrentProxyUrl(),
190
+ totalProxies: proxyRotation.proxies.length,
191
+ currentIndex: proxyRotation.currentIndex,
192
+ requestCount: proxyRotation.requestCount,
193
+ strategy: proxyRotation.rotationStrategy,
194
+ stats: Object.fromEntries(proxyRotation.proxyStats)
195
+ };
196
+ }
197
+ // Global rate limiter configuration
198
+ let rateLimiterConfig = {
199
+ enabled: false,
200
+ requestsPerSecond: 5,
201
+ requestsPerMinute: 100,
202
+ burstLimit: 10,
203
+ domainLimits: new Map(),
204
+ globalCooldown: 100,
205
+ retryAfter: 5000
206
+ };
207
+ // Global rate limiter state
208
+ let rateLimiterState = {
209
+ lastRequestTime: 0,
210
+ requestsInSecond: 0,
211
+ requestsInMinute: 0,
212
+ secondWindowStart: Date.now(),
213
+ minuteWindowStart: Date.now(),
214
+ domainRequestTimes: new Map(),
215
+ isThrottled: false,
216
+ throttleUntil: 0
217
+ };
218
+ /**
219
+ * Initialize rate limiter with custom configuration
220
+ */
221
+ export function initRateLimiter(config = {}) {
222
+ rateLimiterConfig = {
223
+ ...rateLimiterConfig,
224
+ enabled: true,
225
+ ...config
226
+ };
227
+ // Reset state
228
+ rateLimiterState = {
229
+ lastRequestTime: 0,
230
+ requestsInSecond: 0,
231
+ requestsInMinute: 0,
232
+ secondWindowStart: Date.now(),
233
+ minuteWindowStart: Date.now(),
234
+ domainRequestTimes: new Map(),
235
+ isThrottled: false,
236
+ throttleUntil: 0
237
+ };
238
+ console.log(`[RateLimiter] Initialized: ${rateLimiterConfig.requestsPerSecond}/sec, ${rateLimiterConfig.requestsPerMinute}/min`);
239
+ }
240
+ /**
241
+ * Set rate limit for a specific domain
242
+ */
243
+ export function setDomainRateLimit(domain, requestsPerSecond, requestsPerMinute) {
244
+ rateLimiterConfig.domainLimits.set(domain, {
245
+ requestsPerSecond,
246
+ requestsPerMinute: requestsPerMinute || requestsPerSecond * 60
247
+ });
248
+ console.log(`[RateLimiter] Domain limit set for ${domain}: ${requestsPerSecond}/sec`);
249
+ }
250
+ /**
251
+ * Get the delay needed before making a request
252
+ * Returns 0 if request can be made immediately
253
+ */
254
+ export function getRateLimitDelay(url) {
255
+ if (!rateLimiterConfig.enabled)
256
+ return 0;
257
+ const now = Date.now();
258
+ // Check if we're in a throttle period
259
+ if (rateLimiterState.isThrottled && now < rateLimiterState.throttleUntil) {
260
+ return rateLimiterState.throttleUntil - now;
261
+ }
262
+ else if (rateLimiterState.isThrottled) {
263
+ rateLimiterState.isThrottled = false;
264
+ }
265
+ // Update sliding windows
266
+ if (now - rateLimiterState.secondWindowStart >= 1000) {
267
+ rateLimiterState.requestsInSecond = 0;
268
+ rateLimiterState.secondWindowStart = now;
269
+ }
270
+ if (now - rateLimiterState.minuteWindowStart >= 60000) {
271
+ rateLimiterState.requestsInMinute = 0;
272
+ rateLimiterState.minuteWindowStart = now;
273
+ }
274
+ // Check global limits
275
+ if (rateLimiterState.requestsInSecond >= rateLimiterConfig.requestsPerSecond) {
276
+ return 1000 - (now - rateLimiterState.secondWindowStart);
277
+ }
278
+ if (rateLimiterState.requestsInMinute >= rateLimiterConfig.requestsPerMinute) {
279
+ return 60000 - (now - rateLimiterState.minuteWindowStart);
280
+ }
281
+ // Check global cooldown
282
+ const timeSinceLastRequest = now - rateLimiterState.lastRequestTime;
283
+ if (timeSinceLastRequest < rateLimiterConfig.globalCooldown) {
284
+ return rateLimiterConfig.globalCooldown - timeSinceLastRequest;
285
+ }
286
+ // Check domain-specific limits
287
+ if (url) {
288
+ try {
289
+ const domain = new URL(url).hostname;
290
+ const domainLimit = rateLimiterConfig.domainLimits.get(domain);
291
+ if (domainLimit) {
292
+ const domainTimes = rateLimiterState.domainRequestTimes.get(domain) || [];
293
+ // Clean up old timestamps
294
+ const recentTimes = domainTimes.filter(t => now - t < 60000);
295
+ rateLimiterState.domainRequestTimes.set(domain, recentTimes);
296
+ // Check domain limits
297
+ const lastSecondRequests = recentTimes.filter(t => now - t < 1000).length;
298
+ if (lastSecondRequests >= domainLimit.requestsPerSecond) {
299
+ const oldestInSecond = recentTimes.filter(t => now - t < 1000)[0];
300
+ return oldestInSecond ? 1000 - (now - oldestInSecond) : 1000;
301
+ }
302
+ const lastMinuteRequests = recentTimes.length;
303
+ if (lastMinuteRequests >= domainLimit.requestsPerMinute) {
304
+ const oldestInMinute = recentTimes[0];
305
+ return oldestInMinute ? 60000 - (now - oldestInMinute) : 60000;
306
+ }
307
+ }
308
+ }
309
+ catch {
310
+ // Invalid URL, ignore domain limits
311
+ }
312
+ }
313
+ return 0;
314
+ }
315
+ /**
316
+ * Wait for rate limit if needed, then record the request
317
+ */
318
+ export async function waitForRateLimit(url) {
319
+ const delay = getRateLimitDelay(url);
320
+ if (delay > 0) {
321
+ console.log(`[RateLimiter] Waiting ${delay}ms before request${url ? ` to ${new URL(url).hostname}` : ''}`);
322
+ await new Promise(resolve => setTimeout(resolve, delay));
323
+ }
324
+ // Record the request
325
+ recordRequest(url);
326
+ }
327
+ /**
328
+ * Record a request for rate limiting tracking
329
+ */
330
+ export function recordRequest(url) {
331
+ if (!rateLimiterConfig.enabled)
332
+ return;
333
+ const now = Date.now();
334
+ rateLimiterState.lastRequestTime = now;
335
+ rateLimiterState.requestsInSecond++;
336
+ rateLimiterState.requestsInMinute++;
337
+ // Record domain-specific request
338
+ if (url) {
339
+ try {
340
+ const domain = new URL(url).hostname;
341
+ const domainTimes = rateLimiterState.domainRequestTimes.get(domain) || [];
342
+ domainTimes.push(now);
343
+ rateLimiterState.domainRequestTimes.set(domain, domainTimes);
344
+ }
345
+ catch {
346
+ // Invalid URL
347
+ }
348
+ }
349
+ }
350
+ /**
351
+ * Signal that a rate limit response (429) was received
352
+ */
353
+ export function onRateLimitHit(retryAfterMs) {
354
+ const delay = retryAfterMs || rateLimiterConfig.retryAfter;
355
+ rateLimiterState.isThrottled = true;
356
+ rateLimiterState.throttleUntil = Date.now() + delay;
357
+ console.log(`[RateLimiter] Rate limit hit! Throttling for ${delay}ms`);
358
+ }
359
+ /**
360
+ * Get current rate limiter stats
361
+ */
362
+ export function getRateLimiterStats() {
363
+ const now = Date.now();
364
+ return {
365
+ enabled: rateLimiterConfig.enabled,
366
+ requestsInSecond: rateLimiterState.requestsInSecond,
367
+ requestsInMinute: rateLimiterState.requestsInMinute,
368
+ limitsPerSecond: rateLimiterConfig.requestsPerSecond,
369
+ limitsPerMinute: rateLimiterConfig.requestsPerMinute,
370
+ isThrottled: rateLimiterState.isThrottled,
371
+ throttleRemaining: rateLimiterState.isThrottled ? Math.max(0, rateLimiterState.throttleUntil - now) : 0,
372
+ domainLimits: Object.fromEntries(rateLimiterConfig.domainLimits),
373
+ cooldown: rateLimiterConfig.globalCooldown
374
+ };
375
+ }
376
+ /**
377
+ * Disable rate limiter
378
+ */
379
+ export function disableRateLimiter() {
380
+ rateLimiterConfig.enabled = false;
381
+ console.log('[RateLimiter] Disabled');
382
+ }
39
383
  // Browser error categorization
40
384
  export var BrowserErrorType;
41
385
  (function (BrowserErrorType) {