brave-real-browser-mcp-server 2.15.5 → 2.15.7

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.
@@ -0,0 +1,182 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { exec, spawn } from 'child_process';
5
+ import axios from 'axios';
6
+ import { promisify } from 'util';
7
+ const execAsync = promisify(exec);
8
+ export class BraveInstaller {
9
+ static WINDOWS_INSTALLER_URL = 'https://laptop-updates.brave.com/latest/winx64';
10
+ static MAC_INSTALLER_URL = 'https://laptop-updates.brave.com/latest/osx';
11
+ /**
12
+ * Install Brave Browser based on the current platform with Silent/Auto options
13
+ */
14
+ static async install() {
15
+ const platform = process.platform;
16
+ console.error(`⬇️ Attempting to install Brave Browser for ${platform} (Silent Mode)...`);
17
+ try {
18
+ if (platform === 'win32') {
19
+ return await this.installOnWindows();
20
+ }
21
+ else if (platform === 'darwin') {
22
+ return await this.installOnMac();
23
+ }
24
+ else if (platform === 'linux') {
25
+ return await this.installOnLinux();
26
+ }
27
+ return false;
28
+ }
29
+ catch (error) {
30
+ console.error(`❌ Failed to install Brave: ${error.message}`);
31
+ return false;
32
+ }
33
+ }
34
+ /**
35
+ * Install Brave on Windows (Silent Install)
36
+ */
37
+ static async installOnWindows() {
38
+ const tempDir = os.tmpdir();
39
+ const installerPath = path.join(tempDir, 'BraveBrowserSetup.exe');
40
+ console.error('1️⃣ Downloading Brave Installer...');
41
+ try {
42
+ await this.downloadFile(this.WINDOWS_INSTALLER_URL, installerPath);
43
+ }
44
+ catch (e) {
45
+ console.error(` Download failed: ${e.message}`);
46
+ return false;
47
+ }
48
+ console.error('2️⃣ Running Installer (Silent Mode)...');
49
+ try {
50
+ // /silent and /install are standard for Brave/Chrome
51
+ // Triggers UAC prompt if not already elevated, but handles the UI silently
52
+ const installCmd = `"${installerPath}" --silent --install`;
53
+ await execAsync(installCmd);
54
+ console.error('⏳ Waiting for installation to complete...');
55
+ // Wait multiple intervals to ensure filesystem sync
56
+ await new Promise(resolve => setTimeout(resolve, 30000));
57
+ return true;
58
+ }
59
+ catch (error) {
60
+ console.error(' Silent install failed, attempting interactive fallback...');
61
+ try {
62
+ // Fallback: Launch normally so user can interact
63
+ const child = spawn(installerPath, { detached: true, stdio: 'ignore' });
64
+ child.unref();
65
+ return true;
66
+ }
67
+ catch (e) {
68
+ console.error(` Failed to launch installer: ${error.message}`);
69
+ return false;
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Install Brave on Mac (Silent/Automated Install via DMG)
75
+ */
76
+ static async installOnMac() {
77
+ const tempDir = os.tmpdir();
78
+ const dmgPath = path.join(tempDir, 'Brave-Browser.dmg');
79
+ console.error('1️⃣ Downloading Brave DMG...');
80
+ try {
81
+ await this.downloadFile(this.MAC_INSTALLER_URL, dmgPath);
82
+ }
83
+ catch (e) {
84
+ console.error(` Download failed: ${e.message}`);
85
+ return false;
86
+ }
87
+ console.error('2️⃣ Mounting DMG...');
88
+ try {
89
+ // Attach huge image quietly
90
+ await execAsync(`hdiutil attach "${dmgPath}" -nobrowse -quiet`);
91
+ console.error('3️⃣ Copying to Applications...');
92
+ // Copy the app bundle
93
+ try {
94
+ await execAsync(`cp -R "/Volumes/Brave Browser/Brave Browser.app" /Applications/`);
95
+ // Cleanup: Detach
96
+ try {
97
+ await execAsync(`hdiutil detach "/Volumes/Brave Browser" -quiet`);
98
+ }
99
+ catch (e) { }
100
+ // Force attribute update to remove quarantine (avoids 'downloaded from internet' popup)
101
+ try {
102
+ await execAsync(`xattr -r -d com.apple.quarantine "/Applications/Brave Browser.app"`);
103
+ }
104
+ catch (e) { }
105
+ return true;
106
+ }
107
+ catch (e) {
108
+ console.error(' Copy failed (Permission denied?). Opening DMG for user manually...');
109
+ await execAsync(`open "${dmgPath}"`);
110
+ return true;
111
+ }
112
+ }
113
+ catch (error) {
114
+ console.error(` Install failed: ${error.message}`);
115
+ return false;
116
+ }
117
+ }
118
+ /**
119
+ * Install Brave on Linux (Debian/Ubuntu/Fedora support)
120
+ */
121
+ static async installOnLinux() {
122
+ console.error('1️⃣ Detecting Linux distribution...');
123
+ try {
124
+ let dist = 'unknown';
125
+ if (fs.existsSync('/etc/debian_version'))
126
+ dist = 'debian';
127
+ else if (fs.existsSync('/etc/fedora-release'))
128
+ dist = 'fedora';
129
+ // Can extend for Arch/openSUSE if needed
130
+ console.error(` Detected family: ${dist}`);
131
+ if (dist === 'debian') {
132
+ console.error('2️⃣ Installing for Debian/Ubuntu (Attempting sudo)...');
133
+ // Basic check if we are root or can sudo
134
+ const cmd = `
135
+ sudo apt install -y curl &&
136
+ sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg &&
137
+ echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main" | sudo tee /etc/apt/sources.list.d/brave-browser-release.list &&
138
+ sudo apt update &&
139
+ sudo apt install -y brave-browser
140
+ `;
141
+ await execAsync(cmd);
142
+ return true;
143
+ }
144
+ else if (dist === 'fedora') {
145
+ console.error('2️⃣ Installing for Fedora/CentOS (Attempting sudo)...');
146
+ const cmd = `
147
+ sudo dnf install -y dnf-plugins-core &&
148
+ sudo dnf config-manager --add-repo https://brave-browser-rpm-release.s3.brave.com/brave-browser.repo &&
149
+ sudo rpm --import https://brave-browser-rpm-release.s3.brave.com/brave-core.asc &&
150
+ sudo dnf install -y brave-browser
151
+ `;
152
+ await execAsync(cmd);
153
+ return true;
154
+ }
155
+ else {
156
+ console.error(' Unsupported Linux distribution for auto-install.');
157
+ return false;
158
+ }
159
+ }
160
+ catch (error) {
161
+ console.error(` Linux install failed: ${error.message}`);
162
+ console.error(' Note: This requires sudo/root access without password or interactive terminal.');
163
+ return false;
164
+ }
165
+ }
166
+ /**
167
+ * Download a file from URL to destination
168
+ */
169
+ static async downloadFile(url, dest) {
170
+ const writer = fs.createWriteStream(dest);
171
+ const response = await axios({
172
+ url,
173
+ method: 'GET',
174
+ responseType: 'stream'
175
+ });
176
+ response.data.pipe(writer);
177
+ return new Promise((resolve, reject) => {
178
+ writer.on('finish', resolve);
179
+ writer.on('error', reject);
180
+ });
181
+ }
182
+ }
@@ -4,6 +4,7 @@ import * as path from 'path';
4
4
  import * as net from 'net';
5
5
  import { execSync, spawn } from 'child_process';
6
6
  import { config as dotenvConfig } from 'dotenv';
7
+ import { BraveInstaller } from './brave-installer.js';
7
8
  // Load environment variables from .env file
8
9
  // Silence dotenv output
9
10
  const originalWrite = process.stdout.write;
@@ -25,6 +26,12 @@ export var BrowserErrorType;
25
26
  // Store browser instance
26
27
  let browserInstance = null;
27
28
  let pageInstance = null;
29
+ export function setBrowser(browser) {
30
+ browserInstance = browser;
31
+ }
32
+ export function setPage(page) {
33
+ pageInstance = page;
34
+ }
28
35
  // Check environment variable for testing override
29
36
  const disableContentPriority = process.env.DISABLE_CONTENT_PRIORITY === 'true' || process.env.NODE_ENV === 'test';
30
37
  let contentPriorityConfig = {
@@ -431,7 +438,21 @@ export async function initializeBrowser(options) {
431
438
  await closeBrowser();
432
439
  }
433
440
  }
434
- const detectedBravePath = detectBravePath();
441
+ let detectedBravePath = detectBravePath();
442
+ const autoInstall = options?.autoInstall ?? true;
443
+ if (!detectedBravePath && autoInstall) {
444
+ console.error('⚠️ Brave Browser not found. autoInstall is enabled.');
445
+ const installed = await BraveInstaller.install();
446
+ if (installed) {
447
+ console.error('✅ Installation triggered. Retrying detection...');
448
+ // Wait a bit to ensure it's registered
449
+ await new Promise(r => setTimeout(r, 2000));
450
+ detectedBravePath = detectBravePath();
451
+ }
452
+ }
453
+ if (!detectedBravePath && !options?.customConfig?.chromePath) {
454
+ throw new Error('Brave Browser not found and auto-install failed or disabled. Please install Brave Browser manually: https://brave.com/download/');
455
+ }
435
456
  const customConfig = options?.customConfig ?? {};
436
457
  const platform = process.platform;
437
458
  const getOptimalBraveFlags = (isWindows, isRetry = false) => {
@@ -25,11 +25,11 @@ export async function handleExtractJSON(args) {
25
25
  const defaultSelector = selector || 'script[type="application/json"], script[type="application/ld+json"], script';
26
26
  const scripts = document.querySelectorAll(defaultSelector);
27
27
  scripts.forEach((script, index) => {
28
+ const content = script.textContent || '';
28
29
  try {
29
- const content = script.textContent || '';
30
+ // 1. Try direct parsing first
30
31
  const data = JSON.parse(content);
31
32
  if (filter) {
32
- // Simple filter check
33
33
  const filterLower = filter.toLowerCase();
34
34
  const dataStr = JSON.stringify(data).toLowerCase();
35
35
  if (!dataStr.includes(filterLower))
@@ -42,7 +42,37 @@ export async function handleExtractJSON(args) {
42
42
  });
43
43
  }
44
44
  catch (e) {
45
- // Invalid JSON, skip
45
+ // 2. Fallback: Try to find JSON objects using regex
46
+ // Matches { "key": ... } or [ ... ] structures
47
+ const jsonRegex = /({[\s\S]*?}|\[[\s\S]*?\])/g;
48
+ let match;
49
+ while ((match = jsonRegex.exec(content)) !== null) {
50
+ const potentialJson = match[0];
51
+ // Basic heuristic to avoid trying to parse tiny fragments
52
+ if (potentialJson.length < 20)
53
+ continue;
54
+ try {
55
+ const data = JSON.parse(potentialJson);
56
+ // Check filter
57
+ if (filter) {
58
+ const filterLower = filter.toLowerCase();
59
+ const dataStr = JSON.stringify(data).toLowerCase();
60
+ if (!dataStr.includes(filterLower))
61
+ continue;
62
+ }
63
+ // Basic check to ensure it's a nontrivial object/array
64
+ if ((Array.isArray(data) && data.length > 0) || (typeof data === 'object' && data !== null && Object.keys(data).length > 0)) {
65
+ results.push({
66
+ data,
67
+ source: 'script',
68
+ path: `script[${index}]_regex_match`,
69
+ });
70
+ }
71
+ }
72
+ catch (e2) {
73
+ // Not valid JSON
74
+ }
75
+ }
46
76
  }
47
77
  });
48
78
  }
@@ -67,73 +67,6 @@ export async function handleBatchElementScraper(args) {
67
67
  };
68
68
  }, 'Failed to batch scrape elements');
69
69
  }
70
- /**
71
- * Parent-child relationships maintain करते हुए data निकालता है
72
- */
73
- export async function handleNestedDataExtraction(args) {
74
- return await withErrorHandling(async () => {
75
- validateWorkflow('nested_data_extraction', {
76
- requireBrowser: true,
77
- requirePage: true,
78
- });
79
- const page = getCurrentPage();
80
- const parentSelector = args.parentSelector;
81
- const childSelector = args.childSelector;
82
- const maxParents = args.maxParents || 50;
83
- const nestedData = await page.evaluate(({ parentSelector, childSelector, maxParents }) => {
84
- const parents = document.querySelectorAll(parentSelector);
85
- const results = [];
86
- let count = 0;
87
- parents.forEach((parent) => {
88
- if (count >= maxParents)
89
- return;
90
- const parentData = {
91
- selector: parentSelector,
92
- text: Array.from(parent.childNodes)
93
- .filter((node) => node.nodeType === Node.TEXT_NODE)
94
- .map((node) => node.textContent?.trim())
95
- .filter((text) => text)
96
- .join(' '),
97
- attributes: {},
98
- };
99
- // Get parent attributes
100
- Array.from(parent.attributes).forEach((attr) => {
101
- parentData.attributes[attr.name] = attr.value;
102
- });
103
- // Get children
104
- const children = parent.querySelectorAll(childSelector);
105
- const childrenData = [];
106
- children.forEach((child) => {
107
- const childData = {
108
- selector: childSelector,
109
- text: child.textContent?.trim() || '',
110
- attributes: {},
111
- };
112
- Array.from(child.attributes).forEach((attr) => {
113
- childData.attributes[attr.name] = attr.value;
114
- });
115
- childrenData.push(childData);
116
- });
117
- if (childrenData.length > 0) {
118
- results.push({
119
- parent: parentData,
120
- children: childrenData,
121
- });
122
- count++;
123
- }
124
- });
125
- return results;
126
- }, { parentSelector, childSelector, maxParents });
127
- return {
128
- content: [
129
- {
130
- type: 'text',
131
- text: `✅ Extracted ${nestedData.length} parent-child relationships\n\n${JSON.stringify(nestedData, null, 2)}`,
132
- },
133
- ],
134
- };
135
- }, 'Failed to extract nested data');
136
- }
137
70
  /**
138
71
  * सभी elements के attributes (href, src, data-*) collect करता है
139
72
  */
@@ -213,6 +146,10 @@ export async function handleLinkHarvester(args) {
213
146
  const results = [];
214
147
  links.forEach((link, index) => {
215
148
  const href = link.href;
149
+ // Skip if no href (e.g. button without href)
150
+ if (!href) {
151
+ return;
152
+ }
216
153
  // Skip anchors if not included
217
154
  if (!includeAnchors && href.startsWith('#')) {
218
155
  return;
@@ -244,6 +181,7 @@ export async function handleLinkHarvester(args) {
244
181
  }
245
182
  catch (e) {
246
183
  linkInfo.type = 'invalid';
184
+ linkInfo.domain = 'unknown'; // Ensure domain exists even if invalid
247
185
  }
248
186
  }
249
187
  // Additional attributes
@@ -143,3 +143,62 @@ async function withWorkflowValidation(toolName, args, operation) {
143
143
  throw error;
144
144
  }
145
145
  }
146
+ /**
147
+ * Site structure follow करके pages scrape करता है
148
+ */
149
+ export async function handleBreadcrumbNavigator(args) {
150
+ return await withWorkflowValidation('breadcrumb_navigator', args, async () => {
151
+ return await withErrorHandling(async () => {
152
+ const page = getPageInstance();
153
+ if (!page) {
154
+ throw new Error('Browser not initialized. Call browser_init first.');
155
+ }
156
+ const breadcrumbSelector = args.breadcrumbSelector || '.breadcrumb, nav[aria-label="breadcrumb"], .breadcrumbs';
157
+ const followLinks = args.followLinks || false;
158
+ const breadcrumbData = await page.evaluate((selector) => {
159
+ const breadcrumbs = document.querySelectorAll(selector);
160
+ const results = [];
161
+ breadcrumbs.forEach((breadcrumb) => {
162
+ const links = breadcrumb.querySelectorAll('a');
163
+ const items = [];
164
+ links.forEach((link, index) => {
165
+ items.push({
166
+ text: link.textContent?.trim() || '',
167
+ href: link.href,
168
+ level: index,
169
+ });
170
+ });
171
+ if (items.length > 0) {
172
+ results.push({
173
+ path: items.map((i) => i.text).join(' > '),
174
+ links: items,
175
+ });
176
+ }
177
+ });
178
+ return results;
179
+ }, breadcrumbSelector);
180
+ if (breadcrumbData.length === 0) {
181
+ return {
182
+ content: [
183
+ {
184
+ type: 'text',
185
+ text: '❌ No breadcrumbs found on page',
186
+ },
187
+ ],
188
+ };
189
+ }
190
+ let additionalData = '';
191
+ if (followLinks && breadcrumbData[0]?.links) {
192
+ additionalData = `\n\n📌 To scrape breadcrumb pages, use multi_page_scraper with URLs: ${JSON.stringify(breadcrumbData[0].links.map((l) => l.href))}`;
193
+ }
194
+ return {
195
+ content: [
196
+ {
197
+ type: 'text',
198
+ text: `✅ Found ${breadcrumbData.length} breadcrumb trail(s)\n\n${JSON.stringify(breadcrumbData, null, 2)}${additionalData}`,
199
+ },
200
+ ],
201
+ };
202
+ }, 'Failed to navigate breadcrumbs');
203
+ });
204
+ }
@@ -262,124 +262,3 @@ export async function handleAdvancedCSSSelectors(args) {
262
262
  return { content: [{ type: 'text', text: `❌ CSS selector query failed: ${error.message}` }], isError: true };
263
263
  }
264
264
  }
265
- /**
266
- * Visual Element Finder - Find elements by visual properties
267
- */
268
- export async function handleVisualElementFinder(args) {
269
- const { url, criteria } = args;
270
- try {
271
- const page = getPageInstance();
272
- if (!page) {
273
- throw new Error('Browser not initialized. Call browser_init first.');
274
- }
275
- if (url && page.url() !== url) {
276
- await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
277
- }
278
- const results = await page.evaluate((crit) => {
279
- const allElements = Array.from(document.querySelectorAll('*'));
280
- const matches = [];
281
- allElements.forEach(element => {
282
- const computed = window.getComputedStyle(element);
283
- const rect = element.getBoundingClientRect();
284
- let matchScore = 0;
285
- const reasons = [];
286
- // Check visibility
287
- if (crit.visible !== undefined) {
288
- const isVisible = computed.display !== 'none' &&
289
- computed.visibility !== 'hidden' &&
290
- rect.width > 0 &&
291
- rect.height > 0;
292
- if (isVisible === crit.visible) {
293
- matchScore += 10;
294
- reasons.push('visibility');
295
- }
296
- }
297
- // Check color
298
- if (crit.color) {
299
- if (computed.color.includes(crit.color) || computed.backgroundColor.includes(crit.color)) {
300
- matchScore += 5;
301
- reasons.push('color');
302
- }
303
- }
304
- // Check size
305
- if (crit.minWidth && rect.width >= crit.minWidth) {
306
- matchScore += 3;
307
- reasons.push('minWidth');
308
- }
309
- if (crit.maxWidth && rect.width <= crit.maxWidth) {
310
- matchScore += 3;
311
- reasons.push('maxWidth');
312
- }
313
- if (crit.minHeight && rect.height >= crit.minHeight) {
314
- matchScore += 3;
315
- reasons.push('minHeight');
316
- }
317
- if (crit.maxHeight && rect.height <= crit.maxHeight) {
318
- matchScore += 3;
319
- reasons.push('maxHeight');
320
- }
321
- // Check position
322
- if (crit.position) {
323
- if (computed.position === crit.position) {
324
- matchScore += 5;
325
- reasons.push('position');
326
- }
327
- }
328
- // Check text content
329
- if (crit.hasText !== undefined) {
330
- const hasText = (element.textContent?.trim().length || 0) > 0;
331
- if (hasText === crit.hasText) {
332
- matchScore += 5;
333
- reasons.push('hasText');
334
- }
335
- }
336
- // Check if element is in viewport
337
- if (crit.inViewport !== undefined) {
338
- const inViewport = rect.top >= 0 &&
339
- rect.left >= 0 &&
340
- rect.bottom <= window.innerHeight &&
341
- rect.right <= window.innerWidth;
342
- if (inViewport === crit.inViewport) {
343
- matchScore += 5;
344
- reasons.push('inViewport');
345
- }
346
- }
347
- if (matchScore > 0) {
348
- matches.push({
349
- element: {
350
- tagName: element.tagName.toLowerCase(),
351
- id: element.id,
352
- className: element.className,
353
- text: element.textContent?.substring(0, 100)
354
- },
355
- score: matchScore,
356
- matchedCriteria: reasons,
357
- visualProperties: {
358
- display: computed.display,
359
- visibility: computed.visibility,
360
- position: computed.position,
361
- color: computed.color,
362
- backgroundColor: computed.backgroundColor,
363
- width: rect.width,
364
- height: rect.height,
365
- top: rect.top,
366
- left: rect.left
367
- }
368
- });
369
- }
370
- });
371
- matches.sort((a, b) => b.score - a.score);
372
- return {
373
- totalMatches: matches.length,
374
- topMatches: matches.slice(0, 20)
375
- };
376
- }, criteria);
377
- const resultText = `✅ Visual Element Finder Results\n\nCriteria: ${JSON.stringify(criteria, null, 2)}\nTotal Matches: ${results.totalMatches}\n\nTop Matches:\n${JSON.stringify(results.topMatches, null, 2)}`;
378
- return {
379
- content: [{ type: 'text', text: resultText }],
380
- };
381
- }
382
- catch (error) {
383
- return { content: [{ type: 'text', text: `❌ Visual element finder failed: ${error.message}` }], isError: true };
384
- }
385
- }
@@ -307,7 +307,18 @@ export async function handleFetchXHR(args) {
307
307
  });
308
308
  const page = getCurrentPage();
309
309
  const duration = args.duration || 15000;
310
+ const forceReload = args.forceReload !== false; // Default true to capture initial requests
310
311
  const xhrData = [];
312
+ // Capture requests too for completeness
313
+ const requestHandler = (request) => {
314
+ try {
315
+ const resourceType = request.resourceType();
316
+ if (resourceType === 'xhr' || resourceType === 'fetch') {
317
+ // Optional: Log request if needed, but for now we focus on responses with bodies
318
+ }
319
+ }
320
+ catch (e) { }
321
+ };
311
322
  const responseHandler = async (response) => {
312
323
  const request = response.request();
313
324
  const resourceType = request.resourceType();
@@ -320,7 +331,8 @@ export async function handleFetchXHR(args) {
320
331
  statusText: response.statusText(),
321
332
  headers: response.headers(),
322
333
  method: request.method(),
323
- body: body.substring(0, 1000), // First 1000 chars
334
+ postData: request.postData(),
335
+ body: body.substring(0, 5000), // Increased limit
324
336
  timestamp: new Date().toISOString(),
325
337
  });
326
338
  }
@@ -330,6 +342,14 @@ export async function handleFetchXHR(args) {
330
342
  }
331
343
  };
332
344
  page.on('response', responseHandler);
345
+ if (forceReload) {
346
+ try {
347
+ await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
348
+ }
349
+ catch (e) {
350
+ // Continue even if reload times out
351
+ }
352
+ }
333
353
  await sleep(duration);
334
354
  page.off('response', responseHandler);
335
355
  return {