real-browser-mcp-server 1.1.1 → 1.1.3

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/src/mcp/server.js CHANGED
@@ -1,14 +1,3 @@
1
- /**
2
- * Brave Real Browser MCP Server
3
- *
4
- * Model Context Protocol Server with STDIO Transport
5
- * Supports: Claude, Cursor, Copilot, and other MCP-compatible AI assistants
6
- */
7
-
8
- // CRITICAL: Redirect ALL console.log to STDERR before ANY imports
9
- // MCP uses STDIO transport — STDOUT must contain ONLY JSON-RPC messages.
10
- // Any console.log from this code or ANY dependency (puppeteer, blocker, etc.)
11
- // will corrupt the JSON-RPC stream and cause parsing errors.
12
1
  const _originalConsoleLog = console.log;
13
2
  console.log = function (...args) {
14
3
  console.error(...args);
@@ -26,6 +15,14 @@ const {
26
15
  const { TOOLS } = require('./tools.js');
27
16
  const { executeTool, cleanup } = require('./handlers.js');
28
17
 
18
+ // Single source of truth: read version from package.json (avoids version drift)
19
+ let PKG_VERSION = '0.0.0';
20
+ try {
21
+ PKG_VERSION = require('../../package.json').version || PKG_VERSION;
22
+ } catch (e) {
23
+ console.error('⚠️ Could not read version from package.json:', e.message);
24
+ }
25
+
29
26
  /**
30
27
  * Create and configure MCP Server
31
28
  */
@@ -33,7 +30,7 @@ function createServer() {
33
30
  const server = new Server(
34
31
  {
35
32
  name: 'real-browser-mcp-server',
36
- version: '2.48.36',
33
+ version: PKG_VERSION,
37
34
  },
38
35
  {
39
36
  capabilities: {
@@ -1,21 +1,3 @@
1
- /**
2
- * Brave Real Browser MCP Server - Shared Tool Definitions
3
- *
4
- * OPTIMIZED: 22 tools (merged from 28)
5
- *
6
- * Merges Applied:
7
- * - iframe_handler + stream_extractor + player_api_hook → media_extractor
8
- * - get_content + js_scrape → get_content (enhanced)
9
- * - search_regex + extract_json + scrape_meta_tags → extract_data
10
- * - solve_captcha + form_automator → solve_captcha (enhanced)
11
- *
12
- * New Features:
13
- * - URL/Base64/AES Decoders built into media_extractor
14
- * - AI Auto-Healing Selectors
15
- * - Smart Retry Mechanisms
16
- * - Batch Operations
17
- */
18
-
19
1
  const TOOLS = [
20
2
  // 1. Browser Init
21
3
  {
@@ -59,7 +41,7 @@ const TOOLS = [
59
41
  type: 'object',
60
42
  properties: {
61
43
  url: { type: 'string' },
62
- waitUntil: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'], default: 'networkidle2' },
44
+ waitUntil: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle', 'commit'], default: 'networkidle' },
63
45
  timeout: { type: 'number', default: 30000 },
64
46
  retries: { type: 'number', default: 3, description: 'Auto-retry on failures' },
65
47
  smartWait: { type: 'boolean', default: true, description: 'AI-powered smart waiting' }
@@ -575,6 +557,71 @@ const TOOLS = [
575
557
  },
576
558
  required: ['code']
577
559
  }
560
+ },
561
+
562
+ // 23. Screenshot
563
+ {
564
+ name: 'screenshot',
565
+ emoji: '📸',
566
+ description: 'Capture a screenshot of the viewport, the full scrollable page, or a specific element. Returns the image directly to the AI agent (base64) and can optionally save it to a file.',
567
+ descriptionHindi: 'स्क्रीनशॉट लेना — viewport, पूरा पेज, या किसी element का। Image सीधे AI को मिलती है + file में save हो सकती है।',
568
+ category: 'capture',
569
+ requiresBrowser: true,
570
+ requiresPage: true,
571
+ inputSchema: {
572
+ type: 'object',
573
+ properties: {
574
+ fullPage: { type: 'boolean', default: false, description: 'Capture the full scrollable page' },
575
+ selector: { type: 'string', description: 'CSS selector to screenshot a specific element only' },
576
+ format: { type: 'string', enum: ['png', 'jpeg'], default: 'png' },
577
+ quality: { type: 'number', description: 'JPEG quality 0-100 (jpeg only)' },
578
+ path: { type: 'string', description: 'Optional file path to save the screenshot' },
579
+ returnBase64: { type: 'boolean', default: true, description: 'Return image to AI as base64 (MCP image content)' },
580
+ omitBackground: { type: 'boolean', default: false, description: 'Transparent background (png only)' }
581
+ }
582
+ }
583
+ },
584
+
585
+ // 24. Save as PDF
586
+ {
587
+ name: 'save_as_pdf',
588
+ emoji: '📑',
589
+ description: 'Save the current page as a PDF file using Chromium print-to-PDF. Note: works only in headless mode.',
590
+ descriptionHindi: 'पेज को PDF में सेव करना (Chromium print-to-PDF)। ध्यान: सिर्फ़ headless mode में काम करता है।',
591
+ category: 'capture',
592
+ requiresBrowser: true,
593
+ requiresPage: true,
594
+ inputSchema: {
595
+ type: 'object',
596
+ properties: {
597
+ path: { type: 'string', default: './downloads/page.pdf', description: 'File path to save the PDF' },
598
+ format: { type: 'string', default: 'A4', description: 'Paper format: A4, Letter, Legal, etc.' },
599
+ landscape: { type: 'boolean', default: false },
600
+ printBackground: { type: 'boolean', default: true, description: 'Include background graphics' }
601
+ }
602
+ }
603
+ },
604
+
605
+ // 25. See Page (AI Vision — "eyes")
606
+ {
607
+ name: 'see_page',
608
+ emoji: '👁️',
609
+ description: 'AI VISION ("eyes"): Visually SEE the current page exactly like a human does. Captures a screenshot and returns the actual image to the AI agent so it can visually understand the layout, AND returns a "visual map" of all visible interactive elements (buttons, links, inputs) with their on-screen position (x/y/width/height), text label, and a click-ready selector. Use this to look at a page before deciding where to click/type.',
610
+ descriptionHindi: 'AI विज़न ("आँखें"): पेज को इंसान की तरह देखना। स्क्रीनशॉट image सीधे AI को भेजता है ताकि वह layout देख सके + सभी दिखने वाले clickable elements का visual map (position + text + selector) देता है — ताकि AI देखकर तय करे कहाँ क्लिक/टाइप करना है।',
611
+ category: 'vision',
612
+ requiresBrowser: true,
613
+ requiresPage: true,
614
+ inputSchema: {
615
+ type: 'object',
616
+ properties: {
617
+ fullPage: { type: 'boolean', default: false, description: 'See the entire scrollable page (true) or just the current viewport (false)' },
618
+ format: { type: 'string', enum: ['png', 'jpeg'], default: 'jpeg', description: 'Image format (jpeg = smaller, faster for vision)' },
619
+ quality: { type: 'number', default: 70, description: 'JPEG quality 0-100 (lower = smaller image to the AI)' },
620
+ includeElements: { type: 'boolean', default: true, description: 'Include the visual map of interactive elements' },
621
+ maxElements: { type: 'number', default: 60, description: 'Max number of interactive elements to map' },
622
+ path: { type: 'string', description: 'Optional file path to also save the captured image' }
623
+ }
624
+ }
578
625
  }
579
626
  ];
580
627
 
@@ -586,6 +633,8 @@ const CATEGORIES = {
586
633
  extraction: { name: 'Extraction', emoji: '📄', description: 'Content extraction and scraping' },
587
634
  network: { name: 'Network', emoji: '📡', description: 'Network operations' },
588
635
  analysis: { name: 'Analysis', emoji: '🧠', description: 'Page analysis' },
636
+ capture: { name: 'Capture', emoji: '📸', description: 'Screenshots and PDF capture' },
637
+ vision: { name: 'Vision', emoji: '👁️', description: 'AI visual perception (sees pages like human eyes)' },
589
638
  utility: { name: 'Utility', emoji: '🛠️', description: 'Utility tools' }
590
639
  };
591
640
 
package/test/cjs/test.js CHANGED
@@ -30,6 +30,126 @@ test.after(async () => {
30
30
  }
31
31
  });
32
32
 
33
+ test('Human-like Move & Click', async () => {
34
+ await page.goto("https://www.google.com", { timeout: 40000 });
35
+ const selector = 'textarea[name="q"], input[name="q"]';
36
+ await page.realCursor.move(selector);
37
+ await page.realClick(selector);
38
+ assert.ok(true);
39
+ })
40
+
41
+ test('Human-like Typing', async () => {
42
+ await page.goto("https://www.google.com", { timeout: 40000 });
43
+ const selector = 'textarea[name="q"], input[name="q"]';
44
+ await page.realCursor.move(selector);
45
+ await page.realClick(selector);
46
+ await page.type(selector, 'Real Browser MCP Server', { delay: 150 });
47
+ const val = await page.inputValue(selector);
48
+ assert.strictEqual(val, 'Real Browser MCP Server');
49
+ })
50
+
51
+ test('Human-like Scrolling', async () => {
52
+ await page.goto("https://www.google.com/search?q=Real+Browser+MCP+Server", { timeout: 40000, waitUntil: 'domcontentloaded' });
53
+ await new Promise(r => setTimeout(r, 2000));
54
+
55
+ console.log('📜 Scrolling down smoothly and fast (400px)...');
56
+ await page.realScroll(400, 500);
57
+ await new Promise(r => setTimeout(r, 600));
58
+
59
+ console.log('📜 Scrolling down smoothly and fast (300px)...');
60
+ await page.realScroll(300, 400);
61
+ await new Promise(r => setTimeout(r, 600));
62
+
63
+ console.log('📜 Scrolling up smoothly and fast (-500px)...');
64
+ await page.realScroll(-500, 600);
65
+ await new Promise(r => setTimeout(r, 1000));
66
+
67
+ assert.ok(true);
68
+ })
69
+
70
+ test('Form Automation Demonstration', async () => {
71
+ console.log('\n🎬 DEMO: Form Automation');
72
+ try {
73
+ await page.goto('https://httpbin.org/forms/post', { timeout: 30000 });
74
+ console.log('\n4️⃣ Filling out form...');
75
+
76
+ // 1. Customer Name
77
+ await page.type('input[name="custname"]', 'John Doe', { delay: 100 });
78
+ console.log('✅ Customer Name filled');
79
+
80
+ // 2. Telephone
81
+ await page.type('input[name="custtel"]', '+1-555-0199', { delay: 100 });
82
+ console.log('✅ Telephone filled');
83
+
84
+ // 3. Email address
85
+ await page.type('input[name="custemail"]', 'john.doe@example.com', { delay: 100 });
86
+ console.log('✅ Email field filled');
87
+
88
+ // 4. Pizza Size (Radio Button)
89
+ await page.realClick('input[value="medium"]');
90
+ console.log('✅ Pizza Size selected (Medium)');
91
+
92
+ // 5. Pizza Toppings (Checkboxes)
93
+ await page.realClick('input[value="bacon"]');
94
+ await page.realClick('input[value="onion"]');
95
+ console.log('✅ Toppings selected (Bacon, Onion)');
96
+
97
+ // 6. Preferred Delivery Time
98
+ await page.type('input[name="delivery"]', '13:00', { delay: 100 });
99
+ console.log('✅ Delivery time filled');
100
+
101
+ // 7. Delivery Instructions (Comments)
102
+ await page.type('textarea[name="comments"]', 'Leave at the front door, please.', { delay: 100 });
103
+ console.log('✅ Delivery instructions filled');
104
+
105
+ // Wait 2 seconds for visual demonstration
106
+ await new Promise(resolve => setTimeout(resolve, 2000));
107
+
108
+ // 8. Submit Order
109
+ await page.realClick('form button');
110
+ console.log('✅ Form submitted successfully');
111
+
112
+ await new Promise(resolve => setTimeout(resolve, 2000));
113
+ console.log('\n🎉 FORM AUTOMATION COMPLETE!');
114
+ } catch (error) {
115
+ console.error('❌ Form automation test failed:', error);
116
+ throw error;
117
+ }
118
+ })
119
+
120
+ test('Content Strategy Demonstration', async () => {
121
+ console.log('\n🎬 DEMO: Content Analysis & Token Management');
122
+ console.log('👀 Watch browser analyze content from different websites');
123
+ try {
124
+ const testSites = [
125
+ { url: 'https://httpbin.org/html', description: 'Simple HTML page' },
126
+ { url: 'https://example.com', description: 'Minimal content page' }
127
+ ];
128
+
129
+ for (const [index, site] of testSites.entries()) {
130
+ console.log(`\n${index + 2}️⃣ Testing ${site.description}: ${site.url}`);
131
+ await page.goto(site.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
132
+ await new Promise(resolve => setTimeout(resolve, 2000));
133
+
134
+ console.log(` 📄 Getting HTML content...`);
135
+ const htmlContent = await page.content();
136
+ console.log(` ✅ HTML analyzed: ${htmlContent.length} characters`);
137
+
138
+ console.log(` 📝 Getting text content...`);
139
+ const textContent = await page.evaluate(() => document.body.innerText);
140
+ console.log(` ✅ Text analyzed: ${textContent.length} characters`);
141
+
142
+ assert.ok(htmlContent.length > 0);
143
+ assert.ok(textContent.length > 0);
144
+ await new Promise(resolve => setTimeout(resolve, 1500));
145
+ }
146
+ console.log('\n🎉 CONTENT ANALYSIS COMPLETE!');
147
+ } catch (error) {
148
+ console.error('❌ Content strategy test failed:', error);
149
+ throw error;
150
+ }
151
+ })
152
+
33
153
  test('DrissionPage Detector', async () => {
34
154
  await page.goto("https://web.archive.org/web/20240913054632/https://drissionpage.pages.dev/", { timeout: 60000 });
35
155
  await page.realClick("#detector")
@@ -38,7 +158,7 @@ test('DrissionPage Detector', async () => {
38
158
  })
39
159
 
40
160
  test('Sannysoft WebDriver Detector', async () => {
41
- await page.goto("https://bot.sannysoft.com/", { timeout: 60000 });
161
+ await page.goto("https://bot.sannysoft.com/", { timeout: 70000 });
42
162
  await new Promise(r => setTimeout(r, 3000));
43
163
  let result = await page.evaluate(() => {
44
164
  const webdriverEl = document.getElementById('webdriver-result');
@@ -48,7 +168,7 @@ test('Sannysoft WebDriver Detector', async () => {
48
168
  })
49
169
 
50
170
  test('Cloudflare WAF', async () => {
51
- await page.goto("https://nopecha.com/demo/cloudflare", { timeout: 60000 });
171
+ await page.goto("https://nopecha.com/demo/cloudflare", { timeout: 70000 });
52
172
  let verify = null
53
173
  let startDate = Date.now()
54
174
  // Increased timeout to 60 seconds to allow turnstile to be solved
@@ -64,7 +184,7 @@ test('Cloudflare WAF', async () => {
64
184
 
65
185
 
66
186
  test('Cloudflare Turnstile', async () => {
67
- await page.goto("https://2captcha.com/demo/cloudflare-turnstile", { timeout: 60000 });
187
+ await page.goto("https://2captcha.com/demo/cloudflare-turnstile", { timeout: 70000 });
68
188
  await page.waitForSelector('.cf-turnstile')
69
189
  let token = null
70
190
  let startDate = Date.now()
@@ -86,7 +206,7 @@ test('Cloudflare Turnstile', async () => {
86
206
 
87
207
  test('Fingerprint JS Bot Detector', async () => {
88
208
  // Use domcontentloaded + higher timeout to avoid timeout on heavy pages
89
- await page.goto("https://fingerprint.com/products/bot-detection/", { waitUntil: 'domcontentloaded', timeout: 60000 });
209
+ await page.goto("https://fingerprint.com/products/bot-detection/", { waitUntil: 'domcontentloaded', timeout: 70000 });
90
210
  await new Promise(r => setTimeout(r, 5000));
91
211
  const detect = await page.evaluate(() => {
92
212
  // Check for bot detection result in page content
@@ -126,39 +246,11 @@ test('Fingerprint JS Bot Detector', async () => {
126
246
  assert.strictEqual(detect, true, "Fingerprint JS Bot Detector test failed!")
127
247
  })
128
248
 
129
-
130
- // Datadome Bot Detector - Tests against a real Datadome-protected website (hermes.com)
131
- test('Datadome Bot Detector', async (t) => {
132
- // Navigate to hermes.com which is protected by Datadome
133
- await page.goto("https://www.hermes.com/us/en/", { waitUntil: 'domcontentloaded', timeout: 60000 });
134
- await new Promise(r => setTimeout(r, 2000 + Math.random() * 1000));
135
-
136
- // Human-like behavior on the page
137
- await page.realCursor.move('body', { paddingPercentage: 20 });
138
- await new Promise(r => setTimeout(r, 500 + Math.random() * 500));
139
- await page.mouse.wheel({ deltaY: 100 + Math.random() * 100 });
140
- await new Promise(r => setTimeout(r, 500 + Math.random() * 500));
141
-
142
- // Check if main page content loaded (Datadome bypass successful)
143
- // Look for Hermès logo or main navigation elements that only appear after Datadome clears
144
- const check = await page.evaluate(() => {
145
- // Check for Hermès header/logo/navigation elements
146
- const hasLogo = !!document.querySelector('a[href*="hermes"]') || !!document.querySelector('[class*="logo"]');
147
- const hasNav = !!document.querySelector('nav') || !!document.querySelector('[class*="header"]') || !!document.querySelector('[class*="navigation"]');
148
- const hasContent = document.body.innerText.length > 500; // Real page has significant content
149
- const notBlocked = !document.body.innerText.toLowerCase().includes('blocked') &&
150
- !document.body.innerText.toLowerCase().includes('captcha');
151
- return (hasLogo || hasNav) && hasContent && notBlocked;
152
- }).catch(() => false);
153
-
154
- assert.strictEqual(check, true, "Datadome Bot Detector test failed! [This may also be because your ip address has a high spam score. Please try with a clean ip address.]");
155
- })
156
-
157
249
  // If this test fails, please first check if you can access https://antcpt.com/score_detector/
158
250
  // Note: ReCAPTCHA V3 score depends heavily on IP reputation, browser history, and Google's algorithms.
159
251
  // A score >= 0.3 indicates the browser is not detected as an obvious bot.
160
252
  test('Recaptcha V3 Score', async () => {
161
- await page.goto("https://antcpt.com/score_detector/", { timeout: 60000 });
253
+ await page.goto("https://antcpt.com/score_detector/", { timeout: 70000 });
162
254
 
163
255
  // Human-like warm-up interactions before clicking
164
256
  // 1. Random mouse movements using realCursor (Bézier curves via ghost-cursor)
@@ -166,7 +258,7 @@ test('Recaptcha V3 Score', async () => {
166
258
  await new Promise(r => setTimeout(r, 500 + Math.random() * 500));
167
259
 
168
260
  // 2. Scroll down a bit to simulate reading
169
- await page.mouse.wheel({ deltaY: 100 + Math.random() * 100 });
261
+ await page.mouse.wheel(0, 100 + Math.random() * 100);
170
262
  await new Promise(r => setTimeout(r, 800 + Math.random() * 400));
171
263
 
172
264
  // 3. Move mouse towards button area naturally
@@ -187,7 +279,7 @@ test('Recaptcha V3 Score', async () => {
187
279
  // Pixelscan Fingerprint Consistency Check
188
280
  // Checks browser fingerprint consistency, automation detection, and proxy detection
189
281
  test('Pixelscan Fingerprint Check', async () => {
190
- await page.goto("https://pixelscan.net/fingerprint-check", { waitUntil: 'domcontentloaded', timeout: 60000 });
282
+ await page.goto("https://pixelscan.net/fingerprint-check", { waitUntil: 'domcontentloaded', timeout: 70000 });
191
283
 
192
284
  // Poll for the final status. We look specifically at the green header and the fingerprint checker card.
193
285
  let result = false;
@@ -217,43 +309,28 @@ test('Pixelscan Fingerprint Check', async () => {
217
309
  if (!result) await new Promise(r => setTimeout(r, 1000));
218
310
  }
219
311
 
312
+ // Capture diagnostic info before asserting
313
+ const debugInfo = await page.evaluate(() => {
314
+ const statusBar = document.querySelector('.status-content');
315
+ const cards = Array.from(document.querySelectorAll('.checker-card'));
316
+ return {
317
+ statusBarExists: !!statusBar,
318
+ statusText: statusBar ? statusBar.innerText : 'NOT FOUND',
319
+ cardCount: cards.length,
320
+ cardTexts: cards.map(c => c.innerText.substring(0, 200)),
321
+ pageTitle: document.title,
322
+ bodySnippet: document.body.innerText.substring(0, 500)
323
+ };
324
+ }).catch(e => ({ error: e.message }));
325
+ console.log('🔍 Pixelscan Debug Info:', JSON.stringify(debugInfo, null, 2));
326
+
220
327
  // Wait 10 seconds so the user can clearly see the final green scan results on screen
221
328
  await new Promise(r => setTimeout(r, 10000));
222
329
 
223
- assert.strictEqual(result, true, "Pixelscan Fingerprint Check failed! Browser fingerprint is inconsistent or masking was detected.")
330
+ assert.strictEqual(result, true, "Pixelscan Fingerprint Check failed! Browser fingerprint is inconsistent or masking was detected.");
224
331
  })
225
332
 
226
- // CreepJS Deep Fingerprint Analysis
227
- // The most comprehensive fingerprint analyzer - checks lies, headless, stealth, trust score
228
- test('CreepJS Fingerprint Analysis', async () => {
229
- await page.goto("https://abrahamjuliot.github.io/creepjs/", { waitUntil: 'domcontentloaded', timeout: 60000 });
230
- await new Promise(r => setTimeout(r, 15000)); // CreepJS needs time to run all checks
231
-
232
- const result = await page.evaluate(() => {
233
- const pageText = document.body.innerText;
234
333
 
235
- // Check headless detection section
236
- // Look for "0% headless" which means not detected as headless
237
- const headlessSection = pageText.match(/(\d+)%\s*headless/i);
238
- const headlessPercent = headlessSection ? parseInt(headlessSection[1]) : 100;
239
334
 
240
- // Check stealth detection
241
- const stealthSection = pageText.match(/(\d+)%\s*stealth/i);
242
- const stealthPercent = stealthSection ? parseInt(stealthSection[1]) : 100;
243
335
 
244
- // Check lies detection - "0 lies" or low count is good
245
- const liesMatch = pageText.match(/(\d+)\s*lie/i);
246
- const liesCount = liesMatch ? parseInt(liesMatch[1]) : 0;
247
-
248
- return {
249
- headlessPercent,
250
- stealthPercent,
251
- liesCount,
252
- // Pass if: 0% headless, 0% stealth, and lies count is low
253
- passed: headlessPercent === 0 && stealthPercent === 0 && liesCount <= 2
254
- };
255
- }).catch(() => ({ passed: false, headlessPercent: -1, stealthPercent: -1, liesCount: -1 }));
256
336
 
257
- assert.strictEqual(result.passed, true,
258
- `CreepJS Fingerprint Analysis failed! Headless: ${result.headlessPercent}%, Stealth: ${result.stealthPercent}%, Lies: ${result.liesCount}`)
259
- })