real-browser-mcp-server 1.0.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.
@@ -0,0 +1,212 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert');
3
+ const { connect } = require('../../lib/cjs/index.js');
4
+
5
+ const realBrowserOption = {
6
+ turnstile: true,
7
+ headless: false,
8
+ customConfig: {}
9
+ }
10
+
11
+ // Shared browser instance for all tests
12
+ let browser = null;
13
+ let page = null;
14
+
15
+ // Setup - Run once before all tests
16
+ test.before(async () => {
17
+ console.log('🚀 Starting browser for all tests...');
18
+ const result = await connect(realBrowserOption);
19
+ browser = result.browser;
20
+ page = result.page;
21
+ console.log('✅ Browser started successfully');
22
+ });
23
+
24
+ // Teardown - Run once after all tests
25
+ test.after(async () => {
26
+ console.log('🏁 Closing browser after all tests...');
27
+ if (browser) {
28
+ await browser.close();
29
+ console.log('✅ Browser closed successfully');
30
+ }
31
+ });
32
+
33
+ test('DrissionPage Detector', async () => {
34
+ await page.goto("https://web.archive.org/web/20240913054632/https://drissionpage.pages.dev/", { timeout: 60000 });
35
+ await page.realClick("#detector")
36
+ let result = await page.evaluate(() => { return document.querySelector('#isBot span').textContent.includes("not") ? true : false })
37
+ assert.strictEqual(result, true, "DrissionPage Detector test failed!")
38
+ })
39
+
40
+ test('Sannysoft WebDriver Detector', async () => {
41
+ await page.goto("https://bot.sannysoft.com/", { timeout: 60000 });
42
+ await new Promise(r => setTimeout(r, 3000));
43
+ let result = await page.evaluate(() => {
44
+ const webdriverEl = document.getElementById('webdriver-result');
45
+ return webdriverEl && webdriverEl.classList.contains('passed');
46
+ });
47
+ assert.strictEqual(result, true, "Sannysoft WebDriver Detector test failed! Browser detected as bot.")
48
+ })
49
+
50
+ test('Cloudflare WAF', async () => {
51
+ await page.goto("https://nopecha.com/demo/cloudflare", { timeout: 60000 });
52
+ let verify = null
53
+ let startDate = Date.now()
54
+ // Increased timeout to 60 seconds to allow turnstile to be solved
55
+ while (!verify && (Date.now() - startDate) < 50000) {
56
+ verify = await page.evaluate(() => {
57
+ // Check if we passed the challenge - look for main content
58
+ return document.querySelector('.link_row') || document.querySelector('a[href*="nopecha"]') ? true : null
59
+ }).catch(() => null)
60
+ await new Promise(r => setTimeout(r, 2000));
61
+ }
62
+ assert.strictEqual(verify === true, true, "Cloudflare WAF test failed! (Site may be blocking automated access)")
63
+ })
64
+
65
+
66
+ test('Cloudflare Turnstile', async () => {
67
+ await page.goto("https://2captcha.com/demo/cloudflare-turnstile", { timeout: 60000 });
68
+ await page.waitForSelector('.cf-turnstile')
69
+ let token = null
70
+ let startDate = Date.now()
71
+ while (!token && (Date.now() - startDate) < 30000) {
72
+ token = await page.evaluate(() => {
73
+ try {
74
+ let item = document.querySelector('[name="cf-turnstile-response"]')?.value
75
+ return item && item.length > 20 ? item : null
76
+ } catch (e) {
77
+ return null
78
+ }
79
+ })
80
+ await new Promise(r => setTimeout(r, 1000));
81
+ }
82
+ assert.strictEqual(token !== null, true, "Cloudflare turnstile test failed!")
83
+ })
84
+
85
+
86
+
87
+ test('Fingerprint JS Bot Detector', async () => {
88
+ // Use domcontentloaded + higher timeout to avoid timeout on heavy pages
89
+ await page.goto("https://fingerprint.com/products/bot-detection/", { waitUntil: 'domcontentloaded', timeout: 60000 });
90
+ await new Promise(r => setTimeout(r, 5000));
91
+ const detect = await page.evaluate(() => {
92
+ // Check for bot detection result in page content
93
+ const pageText = document.body.innerText.toLowerCase();
94
+
95
+ // If page loaded successfully without bot block, test passes
96
+ // Fingerprint.com shows their product page, not a block page
97
+ const isProductPage = pageText.includes('bot detection') ||
98
+ pageText.includes('fingerprint') ||
99
+ document.querySelector('h1') !== null;
100
+
101
+ // Check if we're NOT blocked (no captcha, no access denied)
102
+ const isNotBlocked = !pageText.includes('access denied') &&
103
+ !pageText.includes('blocked') &&
104
+ !pageText.includes('captcha');
105
+
106
+ // Check in pre/code blocks for notDetected result or in page text
107
+ const preElements = document.querySelectorAll('pre, code');
108
+ for (const el of preElements) {
109
+ if (el.textContent.includes('notDetected') || el.textContent.includes('"result": "notDetected"')) {
110
+ return true;
111
+ }
112
+ }
113
+ // Fallback: check any element with partial class match
114
+ const allElements = document.querySelectorAll('*');
115
+ for (const el of allElements) {
116
+ for (const cls of el.classList) {
117
+ if (cls.includes('botSubTitle') && el.textContent.toLowerCase().includes('not')) {
118
+ return true;
119
+ }
120
+ }
121
+ }
122
+
123
+ // If product page loaded and not blocked, we passed
124
+ return isProductPage && isNotBlocked;
125
+ })
126
+ assert.strictEqual(detect, true, "Fingerprint JS Bot Detector test failed!")
127
+ })
128
+
129
+ // If this test fails, please first check if you can access https://antcpt.com/score_detector/
130
+ // Note: ReCAPTCHA V3 score depends heavily on IP reputation, browser history, and Google's algorithms.
131
+ // A score >= 0.3 indicates the browser is not detected as an obvious bot.
132
+ test('Recaptcha V3 Score', async () => {
133
+ await page.goto("https://antcpt.com/score_detector/", { timeout: 60000 });
134
+
135
+ // Human-like warm-up interactions before clicking
136
+ // 1. Random mouse movements using realCursor (Bézier curves via ghost-cursor)
137
+ await page.realCursor.move('body', { paddingPercentage: 20 });
138
+ await new Promise(r => setTimeout(r, 500 + Math.random() * 500));
139
+
140
+ // 2. Scroll down a bit to simulate reading
141
+ await page.mouse.wheel(0, 100 + Math.random() * 100);
142
+ await new Promise(r => setTimeout(r, 800 + Math.random() * 400));
143
+
144
+ // 3. Move mouse towards button area naturally
145
+ await page.realCursor.move('button', { paddingPercentage: 10 });
146
+ await new Promise(r => setTimeout(r, 300 + Math.random() * 300));
147
+
148
+ // 4. Now click the button
149
+ await page.realClick("button")
150
+ await new Promise(r => setTimeout(r, 5000));
151
+
152
+ const score = await page.evaluate(() => {
153
+ return document.querySelector('big').textContent.replace(/[^0-9.]/g, '')
154
+ })
155
+ // 0.3+ means browser is not obviously a bot. Higher scores depend on IP reputation.
156
+ assert.strictEqual(Number(score) >= 0.3, true, "(please first check if you can access https://antcpt.com/score_detector/.) Recaptcha V3 Score should be >=0.3 (not obviously a bot). Score Result: " + score)
157
+ })
158
+
159
+ // Pixelscan Fingerprint Consistency Check
160
+ // Checks browser fingerprint consistency, automation detection, and proxy detection
161
+ test('Pixelscan Fingerprint Check', async () => {
162
+ await page.goto("https://pixelscan.net/fingerprint-check", { waitUntil: 'domcontentloaded', timeout: 60000 });
163
+
164
+ // Poll for the final status. We look specifically at the green header and the fingerprint checker card.
165
+ let result = false;
166
+ const startTime = Date.now();
167
+ while (!result && (Date.now() - startTime) < 30000) {
168
+ result = await page.evaluate(() => {
169
+ const statusBar = document.querySelector('.status-content');
170
+ if (!statusBar) return false;
171
+
172
+ const statusText = statusBar.innerText.toLowerCase();
173
+ const isConsistent = statusText.includes('consistent') && !statusText.includes('inconsistent');
174
+
175
+ const cards = Array.from(document.querySelectorAll('.checker-card'));
176
+ if (cards.length === 0) return false;
177
+
178
+ // Check if scanning is still in progress
179
+ const isScanning = cards.some(c => c.innerText.toLowerCase().includes('scanning') || c.innerText.toLowerCase().includes('collecting'));
180
+ if (isScanning) return false;
181
+
182
+ // Verify if fingerprint card shows masking detected
183
+ const fingerprintCard = cards.find(c => c.innerText.toLowerCase().includes('fingerprint'));
184
+ if (!fingerprintCard) return false;
185
+ const hasMasking = fingerprintCard.innerText.toLowerCase().includes('masking detected') && !fingerprintCard.innerText.toLowerCase().includes('no masking');
186
+
187
+ return isConsistent && !hasMasking;
188
+ }).catch(() => false);
189
+ if (!result) await new Promise(r => setTimeout(r, 1000));
190
+ }
191
+
192
+ // Capture diagnostic info before asserting
193
+ const debugInfo = await page.evaluate(() => {
194
+ const statusBar = document.querySelector('.status-content');
195
+ const cards = Array.from(document.querySelectorAll('.checker-card'));
196
+ return {
197
+ statusBarExists: !!statusBar,
198
+ statusText: statusBar ? statusBar.innerText : 'NOT FOUND',
199
+ cardCount: cards.length,
200
+ cardTexts: cards.map(c => c.innerText.substring(0, 200)),
201
+ pageTitle: document.title,
202
+ bodySnippet: document.body.innerText.substring(0, 500)
203
+ };
204
+ }).catch(e => ({ error: e.message }));
205
+ console.log('🔍 Pixelscan Debug Info:', JSON.stringify(debugInfo, null, 2));
206
+
207
+ // Wait 10 seconds so the user can clearly see the final green scan results on screen
208
+ await new Promise(r => setTimeout(r, 10000));
209
+
210
+ assert.strictEqual(result, true, "Pixelscan Fingerprint Check failed! Browser fingerprint is inconsistent or masking was detected.");
211
+ })
212
+
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,176 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { connect } from '../../lib/esm/index.mjs';
4
+
5
+ const realBrowserOption = {
6
+ turnstile: true,
7
+ headless: false,
8
+ customConfig: {}
9
+ }
10
+
11
+ // Shared browser instance for all tests
12
+ let browser = null;
13
+ let page = null;
14
+
15
+ // Setup - Run once before all tests
16
+ test.before(async () => {
17
+ console.log('🚀 Starting browser for all tests...');
18
+ const result = await connect(realBrowserOption);
19
+ browser = result.browser;
20
+ page = result.page;
21
+ console.log('✅ Browser started successfully');
22
+ });
23
+
24
+ // Teardown - Run once after all tests
25
+ test.after(async () => {
26
+ console.log('🏁 Closing browser after all tests...');
27
+ if (browser) {
28
+ await browser.close();
29
+ console.log('✅ Browser closed successfully');
30
+ }
31
+ });
32
+
33
+ test('DrissionPage Detector', async () => {
34
+ await page.goto("https://web.archive.org/web/20240913054632/https://drissionpage.pages.dev/", { timeout: 60000 });
35
+ await page.realClick("#detector")
36
+ let result = await page.evaluate(() => { return document.querySelector('#isBot span').textContent.includes("not") ? true : false })
37
+ assert.strictEqual(result, true, "DrissionPage Detector test failed!")
38
+ })
39
+
40
+ test('Sannysoft WebDriver Detector', async () => {
41
+ await page.goto("https://bot.sannysoft.com/", { timeout: 60000 });
42
+ await new Promise(r => setTimeout(r, 3000));
43
+ let result = await page.evaluate(() => {
44
+ const webdriverEl = document.getElementById('webdriver-result');
45
+ return webdriverEl && webdriverEl.classList.contains('passed');
46
+ });
47
+ assert.strictEqual(result, true, "Sannysoft WebDriver Detector test failed! Browser detected as bot.")
48
+ })
49
+
50
+ test('Cloudflare WAF', async () => {
51
+ await page.goto("https://nopecha.com/demo/cloudflare", { timeout: 60000 });
52
+ let verify = null
53
+ let startDate = Date.now()
54
+ while (!verify && (Date.now() - startDate) < 50000) {
55
+ verify = await page.evaluate(() => {
56
+ return document.querySelector('.link_row') || document.querySelector('a[href*="nopecha"]') ? true : null
57
+ }).catch(() => null)
58
+ await new Promise(r => setTimeout(r, 2000));
59
+ }
60
+ assert.strictEqual(verify === true, true, "Cloudflare WAF test failed! (Site may be blocking automated access)")
61
+ })
62
+
63
+
64
+ test('Cloudflare Turnstile', async () => {
65
+ await page.goto("https://2captcha.com/demo/cloudflare-turnstile", { timeout: 60000 });
66
+ await page.waitForSelector('.cf-turnstile')
67
+ let token = null
68
+ let startDate = Date.now()
69
+ while (!token && (Date.now() - startDate) < 30000) {
70
+ token = await page.evaluate(() => {
71
+ try {
72
+ let item = document.querySelector('[name="cf-turnstile-response"]')?.value
73
+ return item && item.length > 20 ? item : null
74
+ } catch (e) {
75
+ return null
76
+ }
77
+ })
78
+ await new Promise(r => setTimeout(r, 1000));
79
+ }
80
+ assert.strictEqual(token !== null, true, "Cloudflare turnstile test failed!")
81
+ })
82
+
83
+
84
+
85
+ test('Fingerprint JS Bot Detector', async () => {
86
+ await page.goto("https://fingerprint.com/products/bot-detection/", { waitUntil: 'domcontentloaded', timeout: 60000 });
87
+ await new Promise(r => setTimeout(r, 5000));
88
+ const detect = await page.evaluate(() => {
89
+ const pageText = document.body.innerText.toLowerCase();
90
+
91
+ const isProductPage = pageText.includes('bot detection') ||
92
+ pageText.includes('fingerprint') ||
93
+ document.querySelector('h1') !== null;
94
+
95
+ const isNotBlocked = !pageText.includes('access denied') &&
96
+ !pageText.includes('blocked') &&
97
+ !pageText.includes('captcha');
98
+
99
+ const preElements = document.querySelectorAll('pre, code');
100
+ for (const el of preElements) {
101
+ if (el.textContent.includes('notDetected') || el.textContent.includes('"result": "notDetected"')) {
102
+ return true;
103
+ }
104
+ }
105
+ const allElements = document.querySelectorAll('*');
106
+ for (const el of allElements) {
107
+ for (const cls of el.classList) {
108
+ if (cls.includes('botSubTitle') && el.textContent.toLowerCase().includes('not')) {
109
+ return true;
110
+ }
111
+ }
112
+ }
113
+
114
+ return isProductPage && isNotBlocked;
115
+ })
116
+ assert.strictEqual(detect, true, "Fingerprint JS Bot Detector test failed!")
117
+ })
118
+
119
+ test('Recaptcha V3 Score', async () => {
120
+ await page.goto("https://antcpt.com/score_detector/", { timeout: 60000 });
121
+
122
+ await page.realCursor.move('body', { paddingPercentage: 20 });
123
+ await new Promise(r => setTimeout(r, 500 + Math.random() * 500));
124
+
125
+ await page.mouse.wheel(0, 100 + Math.random() * 100);
126
+ await new Promise(r => setTimeout(r, 800 + Math.random() * 400));
127
+
128
+ await page.realCursor.move('button', { paddingPercentage: 10 });
129
+ await new Promise(r => setTimeout(r, 300 + Math.random() * 300));
130
+
131
+ await page.realClick("button")
132
+ await new Promise(r => setTimeout(r, 5000));
133
+
134
+ const score = await page.evaluate(() => {
135
+ return document.querySelector('big').textContent.replace(/[^0-9.]/g, '')
136
+ })
137
+ assert.strictEqual(Number(score) >= 0.3, true, "(please first check if you can access https://antcpt.com/score_detector/.) Recaptcha V3 Score should be >=0.3 (not obviously a bot). Score Result: " + score)
138
+ })
139
+
140
+ test('Pixelscan Fingerprint Check', async () => {
141
+ await page.goto("https://pixelscan.net/fingerprint-check", { waitUntil: 'domcontentloaded', timeout: 60000 });
142
+
143
+ // Poll for the final status. We look specifically at the green header and the fingerprint checker card.
144
+ let result = false;
145
+ const startTime = Date.now();
146
+ while (!result && (Date.now() - startTime) < 30000) {
147
+ result = await page.evaluate(() => {
148
+ const statusBar = document.querySelector('.status-content');
149
+ if (!statusBar) return false;
150
+
151
+ const statusText = statusBar.innerText.toLowerCase();
152
+ const isConsistent = statusText.includes('consistent') && !statusText.includes('inconsistent');
153
+
154
+ const cards = Array.from(document.querySelectorAll('.checker-card'));
155
+ if (cards.length === 0) return false;
156
+
157
+ // Check if scanning is still in progress
158
+ const isScanning = cards.some(c => c.innerText.toLowerCase().includes('scanning') || c.innerText.toLowerCase().includes('collecting'));
159
+ if (isScanning) return false;
160
+
161
+ // Verify if fingerprint card shows masking detected
162
+ const fingerprintCard = cards.find(c => c.innerText.toLowerCase().includes('fingerprint'));
163
+ if (!fingerprintCard) return false;
164
+ const hasMasking = fingerprintCard.innerText.toLowerCase().includes('masking detected') && !fingerprintCard.innerText.toLowerCase().includes('no masking');
165
+
166
+ return isConsistent && !hasMasking;
167
+ }).catch(() => false);
168
+ if (!result) await new Promise(r => setTimeout(r, 1000));
169
+ }
170
+
171
+ // Wait 10 seconds so the user can clearly see the final green scan results on screen
172
+ await new Promise(r => setTimeout(r, 10000));
173
+
174
+ assert.strictEqual(result, true, "Pixelscan Fingerprint Check failed! Browser fingerprint is inconsistent or masking was detected.");
175
+ })
176
+
package/test-ua.js ADDED
@@ -0,0 +1,47 @@
1
+ const { connect } = require('./lib/cjs/index.js');
2
+ const { chromium } = require('patchright');
3
+
4
+ async function main() {
5
+ console.log("Launching browser via patchright (headless: true)...");
6
+ const browser = await chromium.launch({
7
+ headless: true,
8
+ executablePath: "C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
9
+ });
10
+
11
+ const context = await browser.newContext();
12
+ const page = await context.newPage();
13
+
14
+ // Connect to CDP session
15
+ const client = await page.context().newCDPSession(page);
16
+
17
+ console.log("Sending Emulation.setUserAgentOverride via CDP...");
18
+ await client.send('Emulation.setUserAgentOverride', {
19
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
20
+ userAgentMetadata: {
21
+ brands: [
22
+ { brand: "Chromium", version: "148" },
23
+ { brand: "Brave", version: "148" },
24
+ { brand: "Not/A)Brand", version: "99" }
25
+ ],
26
+ mobile: false,
27
+ platform: "Windows",
28
+ platformVersion: "10.0.0",
29
+ architecture: "x86",
30
+ model: ""
31
+ }
32
+ });
33
+
34
+ // Navigate to https://example.com
35
+ console.log("\n--- Checking on https://example.com ---");
36
+ await page.goto("https://example.com");
37
+ const isSecureExample = await page.evaluate(() => window.isSecureContext);
38
+ console.log("isSecureContext:", isSecureExample);
39
+ const uaExample = await page.evaluate(() => navigator.userAgent);
40
+ console.log("User Agent:", uaExample);
41
+ const uadExample = await page.evaluate(() => navigator.userAgentData ? navigator.userAgentData.brands : null);
42
+ console.log("User Agent Data Brands:", uadExample);
43
+
44
+ await browser.close();
45
+ }
46
+
47
+ main().catch(console.error);
package/typings.d.ts ADDED
@@ -0,0 +1,85 @@
1
+ declare module "real-browser-mcp-server" {
2
+ import type { Browser, Page } from "patchright";
3
+ import type { GhostCursor } from "ghost-cursor-patchright";
4
+
5
+ export function connect(options?: Options): Promise<ConnectResult>;
6
+
7
+ interface PageWithCursor extends Page {
8
+ realClick: GhostCursor["click"];
9
+ realCursor: GhostCursor;
10
+ }
11
+
12
+ type ConnectResult = {
13
+ browser: Browser;
14
+ page: PageWithCursor;
15
+ /** Blocker instance for advanced usage (null if enableBlocker is false) */
16
+ blocker: BraveBlocker | null;
17
+ };
18
+
19
+ interface Options {
20
+ args?: string[];
21
+ headless?: boolean;
22
+ customConfig?: any;
23
+ proxy?: ProxyOptions;
24
+ turnstile?: boolean;
25
+ connectOption?: any;
26
+ disableXvfb?: boolean;
27
+ ignoreAllFlags?: boolean;
28
+ /** Path to the browser executable (defaults to auto-detected Brave browser or falls back to Chromium) */
29
+ executablePath?: string;
30
+ /** Enable blocker on all pages (default: true) */
31
+ enableBlocker?: boolean;
32
+ /** Blocker configuration options */
33
+ blockerOptions?: BlockerOptions;
34
+ }
35
+
36
+ interface ProxyOptions {
37
+ host: string;
38
+ port: number;
39
+ username?: string;
40
+ password?: string;
41
+ }
42
+
43
+ interface BlockerOptions {
44
+ /** Enable standard network request blocking (Ads/Trackers) - default: true */
45
+ enableAdBlocking?: boolean;
46
+ /** Enable stealth evasions (Navigator, WebGL, etc.) - default: true */
47
+ enableStealth?: boolean;
48
+ /** Enable cosmetic filtering (Element hiding) - default: true */
49
+ enableCosmeticFiltering?: boolean;
50
+ /** Enable advanced redirect and popup blocking - default: true */
51
+ enableRedirectBlocking?: boolean;
52
+ /** Enable scriptlet injection for anti-adblock evasion - default: true */
53
+ enableScriptlets?: boolean;
54
+ /** Path to custom filter list file */
55
+ customFiltersPath?: string;
56
+ /** Enable auto-update of uBlock Origin filters - default: true */
57
+ enableFilterAutoUpdate?: boolean;
58
+ /** Filter updater options */
59
+ filterUpdaterOptions?: FilterUpdaterOptions;
60
+ }
61
+
62
+ interface FilterUpdaterOptions {
63
+ /** Cache duration in milliseconds - default: 24 hours */
64
+ cacheDuration?: number;
65
+ /** Custom filters path */
66
+ customFiltersPath?: string;
67
+ /** Enable verbose logging */
68
+ verbose?: boolean;
69
+ }
70
+
71
+ /** BraveBlocker class for advanced ad/tracker blocking */
72
+ interface BraveBlocker {
73
+ /** Initialize the blocker engine */
74
+ init(): Promise<void>;
75
+ /** Enable blocking on a page */
76
+ enable(page: Page): Promise<void>;
77
+ /** Check if a URL should be blocked */
78
+ shouldBlock(url: string): boolean;
79
+ }
80
+ }
81
+
82
+ declare module "real-browser-mcp-server" {
83
+ export * from "real-browser-mcp-server";
84
+ }
85
+