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/Dockerfile CHANGED
@@ -58,7 +58,6 @@ COPY --from=builder /app/node_modules ./node_modules
58
58
  COPY --from=builder /app/package*.json ./
59
59
  COPY --from=builder /app/lib ./lib
60
60
  COPY --from=builder /app/test ./test
61
- COPY --from=builder /app/packages ./packages
62
61
  COPY --from=builder /app/typings.d.ts ./
63
62
 
64
63
  # Set ownership to non-root user
package/README.md CHANGED
@@ -11,11 +11,31 @@ This server is **100% compatible with all major AI IDEs** (Cursor, VS Code, Clin
11
11
 
12
12
  ---
13
13
 
14
+ ## ⚙️ Installation & Setup
15
+
16
+ To install and run the server locally, clone the repository, install NPM dependencies, and configure the undetected browser binary using **Patchright**:
17
+
18
+ ```bash
19
+ # 1. Clone the repository
20
+ git clone https://github.com/codeiva4u/Real-Browser-Mcp-Server.git
21
+
22
+ # 2. Navigate to the project directory
23
+ cd Real-Browser-Mcp-Server
24
+
25
+ # 3. Install dependencies
26
+ npm install
27
+
28
+ # 4. Install Chromium-Driver for Patchright (Undetected Browser binary)
29
+ npx patchright install chromium
30
+ ```
31
+
32
+ ---
33
+
14
34
  ## 🚀 Key Evasion & Stealth Features
15
35
 
16
36
  * **Undetected Browser Engine**: Powered by **Patchright Chromium**, bypassing modern fingerprinting checks (does not expose automation indicators or Webdriver/BiDi flags).
17
37
  * **Integrated Ad & Tracker Blocker**: Utilizes `@ghostery/adblocker-playwright` with asynchronous pre-compiled filter caching to `adblocker.bin`, blocking ads and speed-bumps completely offline.
18
- * **Human-like Interactions**: Integrates **playwright-ghost** (Bézier curves auto-hooking) to transparently simulate human mouse movements, velocity, and natural hover-before-click behaviors.
38
+ * **Human-like Interactions**: Integrates **ghost-cursor-patchright** (Bézier curves) to transparently simulate human mouse movements, velocity, and natural hover-before-click behaviors. Features **Physics-based Smooth Scrolling** (`page.realScroll`) utilizing real mouse-wheel events and Cubic Ease-Out deceleration to perfectly mimic manual trackpad/mouse flicks, bypassing advanced behavioral detectors.
19
39
  * **Turnstile Auto-Solver**: Seamlessly detects and bypasses Cloudflare Turnstile widgets.
20
40
  * **Anti-Race Condition Guards**: Robust state-guards ensure popup blockers, shims, and adblockers attach exactly once per page, preventing context destruction.
21
41
 
@@ -60,14 +80,17 @@ Add the server entry to your global MCP settings file (typically found at `%APPD
60
80
  {
61
81
  "mcpServers": {
62
82
  "real-browser-mcp-server": {
63
- "command": "node",
83
+ "type": "stdio",
84
+ "command": "C:/Program Files/nodejs/node.exe",
64
85
  "args": [
65
86
  "c:/Users/Admin/Desktop/Software/Real-Browser-Mcp-Server/src/index.js"
66
87
  ],
67
88
  "env": {
68
89
  "HEADLESS": "false"
69
90
  },
70
- "disabled": false
91
+ "disabled": false,
92
+ "autoApprove": [],
93
+ "timeout": 120
71
94
  }
72
95
  }
73
96
  }
@@ -116,9 +139,9 @@ Configure the server in your `opencode.jsonc` or standard MCP settings configura
116
139
 
117
140
  ---
118
141
 
119
- ## 🌐 Complete MCP Tool Reference (22 Tools)
142
+ ## 🌐 Complete MCP Tool Reference (25 Tools)
120
143
 
121
- The server exposes 22 highly optimized tools categorized into functional units:
144
+ The server exposes 25 highly optimized tools categorized into functional units:
122
145
 
123
146
  ### 🌐 Browser & Session
124
147
  | Tool Name | Description | Parameters |
@@ -138,7 +161,7 @@ The server exposes 22 highly optimized tools categorized into functional units:
138
161
  | `click` | Human-like click using AI healing, ghost cursor, and iframe support. | `selector` (string), `hoverFirst` (boolean) |
139
162
  | `type` | Type text with human speed variation, smart clearing, and iframe support. | `selector` (string), `text` (string) |
140
163
  | `solve_captcha` | Auto-solve CAPTCHAs (Turnstile, reCAPTCHA, hCaptcha, OCR). | `selector` (string) |
141
- | `random_scroll` | Simulated human scrolling with natural patterns and lazy-load triggers. | `direction` (string), `amount` (number) |
164
+ | `random_scroll` | Simulated human scrolling with natural patterns and lazy-load triggers. | `direction` (string), `amount` (number), `smooth` (boolean) |
142
165
  | `press_key` | Press keyboard keys with modifier key support (Ctrl/Shift/Alt). | `key` (string), `modifiers` (array) |
143
166
  | `execute_js` | Run custom asynchronous/synchronous JavaScript inside a page or iframe. | `code` (string), `iframeIndex` (number) |
144
167
 
@@ -162,6 +185,17 @@ The server exposes 22 highly optimized tools categorized into functional units:
162
185
  | `wait` | Smart delay with AI prediction or static timeout. | `duration` (number) |
163
186
  | `progress_tracker` | Track running automation progress with AI-estimated remaining times. | `step` (string), `percentage` (number) |
164
187
 
188
+ ### 📸 Capture
189
+ | Tool Name | Description | Parameters |
190
+ |:---|:---|:---|
191
+ | `screenshot` | Capture viewport, full page, or a specific element. Returns the image to the AI agent (base64) and can save to a file. | `fullPage` (boolean), `selector` (string), `format` (png/jpeg), `path` (string) |
192
+ | `save_as_pdf` | Save the current page as a PDF via Chromium print-to-PDF (headless mode only). | `path` (string), `format` (string), `landscape` (boolean) |
193
+
194
+ ### 👁️ AI Vision (Eyes)
195
+ | Tool Name | Description | Parameters |
196
+ |:---|:---|:---|
197
+ | `see_page` | Lets the AI **visually SEE** the page like human eyes: returns the actual screenshot image to the agent **plus** a "visual map" of every visible interactive element (button/link/input) with its on-screen position, text label, and a click-ready selector. Use it to look before deciding where to click/type. | `fullPage` (boolean), `format` (png/jpeg), `includeElements` (boolean), `maxElements` (number) |
198
+
165
199
  ---
166
200
 
167
201
  ## 📈 Evasion Performance & Test Coverage
@@ -180,6 +214,39 @@ Our test suites run headless/headed simulations against all major fingerprinting
180
214
  | **CreepJS Fingerprinting** | Advanced trust rating and fingerprint check | ✅ Pass |
181
215
  | **Pixelscan Fingerprint** | Masque & Canvas fingerprint masking check | ✅ Pass (No Masking Detected) |
182
216
 
217
+ ### 🧪 Local Test Suite Execution Status
218
+
219
+ Both the CommonJS and ES Module test suites execute and pass successfully under Node.js:
220
+
221
+ | Test Suite / Environment | Test Case | Status |
222
+ |:---|:---|:---|
223
+ | **CommonJS (`cjs_test`)** | DrissionPage Detector | ✅ Passed |
224
+ | **CommonJS (`cjs_test`)** | Sannysoft WebDriver Detector | ✅ Passed |
225
+ | **CommonJS (`cjs_test`)** | Cloudflare WAF | ✅ Passed |
226
+ | **CommonJS (`cjs_test`)** | Cloudflare Turnstile | ✅ Passed |
227
+ | **CommonJS (`cjs_test`)** | Fingerprint JS Bot Detector | ✅ Passed |
228
+ | **CommonJS (`cjs_test`)** | Recaptcha V3 Score | ✅ Passed |
229
+ | **CommonJS (`cjs_test`)** | Pixelscan Fingerprint Check | ✅ Passed |
230
+ | **CommonJS (`cjs_test`)** | Human-like Move & Click | ✅ Passed |
231
+ | **CommonJS (`cjs_test`)** | Human-like Typing | ✅ Passed |
232
+ | **CommonJS (`cjs_test`)** | Human-like Scrolling | ✅ Passed |
233
+ | **CommonJS (`cjs_test`)** | Form Automation Demonstration | ✅ Passed |
234
+ | **CommonJS (`cjs_test`)** | Content Strategy Demonstration | ✅ Passed |
235
+ | **ES Module (`esm_test`)** | DrissionPage Detector | ✅ Passed |
236
+ | **ES Module (`esm_test`)** | Sannysoft WebDriver Detector | ✅ Passed |
237
+ | **ES Module (`esm_test`)** | Cloudflare WAF | ✅ Passed |
238
+ | **ES Module (`esm_test`)** | Cloudflare Turnstile | ✅ Passed |
239
+ | **ES Module (`esm_test`)** | Fingerprint JS Bot Detector | ✅ Passed |
240
+ | **ES Module (`esm_test`)** | Recaptcha V3 Score | ✅ Passed |
241
+ | **ES Module (`esm_test`)** | Pixelscan Fingerprint Check | ✅ Passed |
242
+ | **ES Module (`esm_test`)** | Human-like Move & Click | ✅ Passed |
243
+ | **ES Module (`esm_test`)** | Human-like Typing | ✅ Passed |
244
+ | **ES Module (`esm_test`)** | Human-like Scrolling | ✅ Passed |
245
+ | **ES Module (`esm_test`)** | Form Automation Demonstration | ✅ Passed |
246
+ | **ES Module (`esm_test`)** | Content Strategy Demonstration | ✅ Passed |
247
+
248
+ ---
249
+
183
250
  ---
184
251
 
185
252
  ## 💻 Programmatic Usage (Node.js SDK)
@@ -201,6 +268,9 @@ const { connect } = require('real-browser-mcp-server');
201
268
  // Real mouse movement and click
202
269
  await page.realClick('#my-button');
203
270
 
271
+ // Real human-like smooth scrolling (60FPS Cubic Ease-Out physics)
272
+ await page.realScroll(400); // scrolls down 400px smoothly
273
+
204
274
  await browser.close();
205
275
  })();
206
276
  ```
@@ -216,6 +286,10 @@ const { browser, page } = await connect({
216
286
 
217
287
  await page.goto('https://example.com');
218
288
  await page.realClick('#my-button');
289
+
290
+ // Real human-like smooth scrolling (60FPS Cubic Ease-Out physics)
291
+ await page.realScroll(400); // scrolls down 400px smoothly
292
+
219
293
  await browser.close();
220
294
  ```
221
295
 
@@ -231,11 +305,12 @@ Run these scripts from the project root directory:
231
305
  | `npm run dev` | Alias to start the MCP server. |
232
306
  | `npm run mcp` | Start the MCP server. |
233
307
  | `npm run mcp:verbose` | Start the MCP server with verbose logging on `stderr`. |
234
- | `npm run list` | Clean list of all 22 tools with emojis and categories. |
308
+ | `npm run list` | Clean list of all 25 tools with emojis and categories. |
235
309
  | `npm run build` | Validate workspace structure and confirm library status. |
236
310
  | `npm test` | Execute the full test suite (CJS & ESM). |
237
311
  | `npm run cjs_test` | Run CommonJS test scripts. |
238
312
  | `npm run esm_test` | Run ECMAScript Module test scripts. |
313
+ | `npm run mcp_test` | Fast, network-independent MCP smoke test (handshake + tool registry validation). |
239
314
 
240
315
  ---
241
316
 
package/lib/cjs/index.js CHANGED
@@ -1,9 +1,7 @@
1
+ const { chromium } = require("patchright");
2
+ const { createCursor } = require("ghost-cursor-patchright");
1
3
  const { PlaywrightBlocker } = require("@ghostery/adblocker-playwright");
2
4
  const { pageController } = require("./module/pageController.js");
3
-
4
- // Dynamically load pure ESM playwright-ghost to be fully compatible with CommonJS
5
- const playwrightGhostPromise = import("playwright-ghost/patchright");
6
- const recommendedPromise = import("playwright-ghost/plugins/recommended");
7
5
  const fs = require('fs');
8
6
  const path = require('path');
9
7
 
@@ -68,13 +66,28 @@ function loadEnvFile() {
68
66
  loadEnvFile();
69
67
 
70
68
  function getDefaultHeadless() {
71
- const envHeadless = (process.env.HEADLESS || '').toLowerCase();
72
- return envHeadless === 'true';
69
+ const envHeadless = process.env.HEADLESS;
70
+ if (envHeadless !== undefined && envHeadless !== null && envHeadless !== '') {
71
+ const value = envHeadless.toLowerCase().trim();
72
+ return value === 'true' || value === '1' || value === 'yes';
73
+ }
74
+ // Auto-detect CI environments
75
+ if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.TRAVIS || process.env.CIRCLECI) {
76
+ return true;
77
+ }
78
+ // Auto-detect headless Linux environments without X11 or Wayland
79
+ if (process.platform === 'linux') {
80
+ const hasDisplay = process.env.DISPLAY || process.env.WAYLAND_DISPLAY;
81
+ if (!hasDisplay) {
82
+ return true;
83
+ }
84
+ }
85
+ return false;
73
86
  }
74
87
 
75
- function applyPuppeteerShims(browser, page) {
76
- if (page._shimsApplied) return page;
77
- page._shimsApplied = true;
88
+ function setupRealPage(browser, page) {
89
+ if (page._setupApplied) return page;
90
+ page._setupApplied = true;
78
91
 
79
92
  // Enable ad blocker
80
93
  if (adBlockerInstance) {
@@ -87,93 +100,165 @@ function applyPuppeteerShims(browser, page) {
87
100
  });
88
101
  }
89
102
 
90
- // Cookie shims (Puppeteer Playwright context)
91
- if (!page.cookies) {
92
- page.cookies = async (urls) => {
93
- return await page.context().cookies(urls ? (Array.isArray(urls) ? urls : [urls]) : []);
94
- };
95
- }
96
-
97
- if (!page.setCookie) {
98
- page.setCookie = async (...cookies) => {
99
- const formatted = cookies.map(c => ({
100
- name: c.name,
101
- value: c.value,
102
- domain: c.domain || new URL(page.url()).hostname,
103
- path: c.path || '/',
104
- expires: c.expires || undefined,
105
- httpOnly: c.httpOnly,
106
- secure: c.secure,
107
- sameSite: c.sameSite
108
- }));
109
- await page.context().addCookies(formatted);
110
- };
111
- }
103
+ // Human-like smooth scrolling with 60FPS Cubic Ease-Out physics
104
+ page.realScroll = async (deltaY, duration = 600) => {
105
+ try {
106
+ const stepDelay = 15; // ~60 FPS
107
+ const steps = Math.max(10, Math.floor(duration / stepDelay));
108
+ const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
109
+ let currentScroll = 0;
110
+ for (let i = 1; i <= steps; i++) {
111
+ const t = i / steps;
112
+ const targetScroll = deltaY * easeOutCubic(t);
113
+ const diff = targetScroll - currentScroll;
114
+ await page.mouse.wheel(0, diff);
115
+ currentScroll = targetScroll;
116
+ await new Promise(r => setTimeout(r, stepDelay));
117
+ }
118
+ } catch (e) {
119
+ // Fallback to native window scroll in case of wheel errors
120
+ try {
121
+ await page.evaluate((y) => window.scrollBy({ top: y, behavior: 'smooth' }), deltaY);
122
+ } catch (_) {}
123
+ }
124
+ };
112
125
 
113
- if (!page.deleteCookie) {
114
- page.deleteCookie = async (...cookies) => {
115
- await page.context().clearCookies();
126
+ // Ghost Cursor integration - Bézier curve human-like mouse movement
127
+ try {
128
+ const cursor = createCursor(page);
129
+ page.realCursor = {
130
+ move: async (selector, options = {}) => {
131
+ try {
132
+ await cursor.actions.move(selector, options);
133
+ } catch (e) {
134
+ // Fallback to native hover if ghost-cursor fails
135
+ try { await page.hover(selector); } catch (_) {}
136
+ }
137
+ }
116
138
  };
117
- }
118
-
119
- // Navigation shims
120
- if (!page.waitForNetworkIdle) {
121
- page.waitForNetworkIdle = async (options = {}) => {
139
+ page.realClick = async (selector, options = {}) => {
122
140
  try {
123
- await page.waitForLoadState('networkidle', { timeout: options.timeout });
141
+ await cursor.actions.click({ target: selector, ...options });
124
142
  } catch (e) {
125
- // Ignore networkidle timeouts if they are non-fatal
143
+ // Fallback to native click if ghost-cursor fails
144
+ await page.click(selector, options);
126
145
  }
127
146
  };
147
+ } catch (e) {
148
+ // Fallback if ghost-cursor-patchright fails to initialize
149
+ if (!page.realClick) {
150
+ page.realClick = async (selector, options) => {
151
+ await page.click(selector, options);
152
+ };
153
+ }
154
+ if (!page.realCursor) {
155
+ page.realCursor = {
156
+ move: async (selector) => {
157
+ try { await page.hover(selector); } catch (_) {}
158
+ }
159
+ };
160
+ }
128
161
  }
129
162
 
130
- if (!page.evaluateOnNewDocument) {
131
- page.evaluateOnNewDocument = async (fn, ...args) => {
132
- return await page.addInitScript(fn, ...args);
133
- };
163
+ return page;
164
+
165
+ }
166
+
167
+ function getBraveExecutablePath() {
168
+ if (process.env.BRAVE_PATH && fs.existsSync(process.env.BRAVE_PATH)) {
169
+ return process.env.BRAVE_PATH;
134
170
  }
135
171
 
136
- // Ghost Cursor integration via playwright-ghost auto-hooking
137
- page.realCursor = {
138
- move: async (selector, options = {}) => {
172
+ const platform = process.platform;
173
+ const { execSync } = require('child_process');
174
+
175
+ // Try automatic scanning via CLI / registry query
176
+ if (platform === 'win32') {
177
+ const regQueries = [
178
+ 'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\brave.exe" /ve',
179
+ 'reg query "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\brave.exe" /ve',
180
+ 'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\StartMenuInternet\\Brave-Browser\\shell\\open\\command" /ve'
181
+ ];
182
+
183
+ for (const cmd of regQueries) {
139
184
  try {
140
- await page.hover(selector, options);
141
- } catch (e) {
142
- // Silently fallback if element is not found
143
- }
185
+ const output = execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
186
+ const match = output.match(/REG_SZ\s+(.*)/);
187
+ if (match && match[1]) {
188
+ let p = match[1].trim().replace(/^"|"$/g, '');
189
+ if (!p.toLowerCase().endsWith('.exe')) {
190
+ const exeIndex = p.toLowerCase().indexOf('.exe');
191
+ if (exeIndex !== -1) {
192
+ p = p.substring(0, exeIndex + 4).replace(/^"|"$/g, '');
193
+ }
194
+ }
195
+ if (fs.existsSync(p)) return p;
196
+ }
197
+ } catch (e) {}
144
198
  }
145
- };
146
- page.realClick = async (selector, options = {}) => {
147
- await page.click(selector, options);
148
- };
149
199
 
150
- // Mouse wheel shim (Puppeteer object → Playwright positional args)
151
- if (page.mouse && page.mouse.wheel) {
152
- const originalWheel = page.mouse.wheel.bind(page.mouse);
153
- page.mouse.wheel = async (x, y) => {
154
- if (typeof x === 'object' && x !== null) {
155
- const deltaX = x.deltaX || 0;
156
- const deltaY = x.deltaY || 0;
157
- return await originalWheel(deltaX, deltaY);
200
+ try {
201
+ const output = execSync('where brave.exe', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim().split('\r\n')[0];
202
+ if (output && fs.existsSync(output)) return output;
203
+ } catch (e) {}
204
+ } else if (platform === 'darwin') {
205
+ try {
206
+ const output = execSync('mdfind "kMDItemCFBundleIdentifier == \'com.brave.Browser\'"', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim().split('\n')[0];
207
+ if (output) {
208
+ const p = path.join(output, 'Contents', 'MacOS', 'Brave Browser');
209
+ if (fs.existsSync(p)) return p;
158
210
  }
159
- return await originalWheel(x, y);
160
- };
211
+ } catch (e) {}
212
+ } else {
213
+ try {
214
+ const output = execSync('which brave-browser || which brave', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
215
+ if (output && fs.existsSync(output)) return output;
216
+ } catch (e) {}
161
217
  }
162
218
 
163
- // Browser shims
164
- if (!browser.process) {
165
- browser.process = () => ({
166
- pid: browser._childProcess?.pid || 1337
167
- });
219
+ // Fallback to hardcoded common paths
220
+ let paths = [];
221
+ if (platform === 'win32') {
222
+ paths = [
223
+ path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
224
+ path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
225
+ path.join(process.env.LOCALAPPDATA || '', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe')
226
+ ].filter(p => p);
227
+ } else if (platform === 'darwin') {
228
+ paths = [
229
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'
230
+ ];
231
+ } else {
232
+ paths = [
233
+ '/usr/bin/brave-browser',
234
+ '/usr/bin/brave',
235
+ '/usr/bin/brave-browser-stable',
236
+ '/usr/bin/brave-browser-beta',
237
+ '/usr/bin/brave-browser-nightly',
238
+ '/usr/local/bin/brave-browser',
239
+ '/usr/local/bin/brave'
240
+ ];
168
241
  }
169
242
 
170
- if (!browser.pages) {
171
- browser.pages = async () => {
172
- return browser.contexts().flatMap(c => c.pages());
173
- };
243
+ for (const p of paths) {
244
+ if (p && fs.existsSync(p)) {
245
+ return p;
246
+ }
174
247
  }
175
248
 
176
- return page;
249
+ return null;
250
+ }
251
+
252
+ async function applyUserAgentOverride(page, userAgent, userAgentMetadata) {
253
+ try {
254
+ const client = await page.context().newCDPSession(page);
255
+ await client.send('Emulation.setUserAgentOverride', {
256
+ userAgent: userAgent,
257
+ userAgentMetadata: userAgentMetadata
258
+ });
259
+ } catch (e) {
260
+ // Ignore errors
261
+ }
177
262
  }
178
263
 
179
264
  async function connect({
@@ -181,6 +266,7 @@ async function connect({
181
266
  headless = getDefaultHeadless(),
182
267
  proxy = {},
183
268
  turnstile = false,
269
+ executablePath = undefined,
184
270
  } = {}) {
185
271
  let playwrightProxy = undefined;
186
272
  if (proxy && proxy.host && proxy.port) {
@@ -193,23 +279,81 @@ async function connect({
193
279
  }
194
280
  }
195
281
 
282
+ // 1. Launch a temporary browser to retrieve the native user agent and properties
283
+ const tempBrowser = await chromium.launch({
284
+ headless: true,
285
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
286
+ ...(executablePath ? { executablePath } : {}),
287
+ });
288
+ const tempContext = await tempBrowser.newContext();
289
+ const tempPage = await tempContext.newPage();
290
+ let nativeUa = '';
291
+ let isBrave = false;
292
+ try {
293
+ nativeUa = await tempPage.evaluate(() => navigator.userAgent);
294
+ isBrave = await tempPage.evaluate(() => typeof navigator.brave !== 'undefined');
295
+ } catch (e) {
296
+ nativeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36';
297
+ isBrave = executablePath && executablePath.toLowerCase().includes('brave');
298
+ }
299
+ await tempBrowser.close();
300
+
301
+ let modifiedUa = nativeUa.replace(/HeadlessChrome\//g, 'Chrome/');
302
+ const chromeVersionMatch = modifiedUa.match(/Chrome\/([\d.]+)/);
303
+ const chromeVersion = chromeVersionMatch ? chromeVersionMatch[1] : '148.0.0.0';
304
+ const majorVersion = chromeVersion.split('.')[0];
305
+
306
+ const brands = [
307
+ { brand: 'Chromium', version: majorVersion },
308
+ { brand: 'Not/A)Brand', version: '99' }
309
+ ];
310
+ if (isBrave) {
311
+ brands.unshift({ brand: 'Brave', version: majorVersion });
312
+ } else {
313
+ brands.unshift({ brand: 'Google Chrome', version: majorVersion });
314
+ }
315
+
316
+ let platformName = 'Windows';
317
+ if (nativeUa.includes('Macintosh') || nativeUa.includes('Mac OS X')) {
318
+ platformName = 'macOS';
319
+ } else if (nativeUa.includes('Linux')) {
320
+ platformName = 'Linux';
321
+ }
322
+
323
+ const userAgentMetadata = {
324
+ brands: brands,
325
+ mobile: false,
326
+ platform: platformName,
327
+ platformVersion: platformName === 'macOS' ? '14.0.0' : platformName === 'Linux' ? '6.0.0' : '10.0.0',
328
+ architecture: 'x86',
329
+ model: '',
330
+ bitness: '64',
331
+ wow64: false
332
+ };
333
+
196
334
  const chromiumArgs = [
335
+ `--user-agent=${modifiedUa}`,
197
336
  '--disable-blink-features=AutomationControlled',
198
337
  '--no-sandbox',
199
338
  '--disable-setuid-sandbox',
200
339
  ...args
201
340
  ];
202
341
 
203
- // Load the dynamic ES Module imports
204
- const { default: playwright } = await playwrightGhostPromise;
205
- const { default: recommended } = await recommendedPromise;
206
- const { chromium } = playwright;
342
+ // If headless is true, we run with headless: false but pass '--headless=new' to args.
343
+ // This triggers Chromium's modern undetected headless mode instead of Playwright's default old headless shell.
344
+ let launchHeadless = headless;
345
+ if (headless === true) {
346
+ launchHeadless = false;
347
+ if (!chromiumArgs.includes('--headless=new')) {
348
+ chromiumArgs.push('--headless=new');
349
+ }
350
+ }
207
351
 
208
352
  const browser = await chromium.launch({
209
- headless,
353
+ headless: launchHeadless,
210
354
  args: chromiumArgs,
211
355
  proxy: playwrightProxy,
212
- plugins: [recommended()]
356
+ ...(executablePath ? { executablePath } : {}),
213
357
  });
214
358
 
215
359
  // Ensure ad blocker is ready
@@ -221,7 +365,9 @@ async function connect({
221
365
 
222
366
  let page = await context.newPage();
223
367
 
224
- applyPuppeteerShims(browser, page);
368
+ await applyUserAgentOverride(page, modifiedUa, userAgentMetadata);
369
+
370
+ setupRealPage(browser, page);
225
371
 
226
372
  page = await pageController({
227
373
  browser,
@@ -231,7 +377,8 @@ async function connect({
231
377
  });
232
378
 
233
379
  context.on('page', async (newPage) => {
234
- applyPuppeteerShims(browser, newPage);
380
+ await applyUserAgentOverride(newPage, modifiedUa, userAgentMetadata);
381
+ setupRealPage(browser, newPage);
235
382
  await pageController({
236
383
  browser,
237
384
  page: newPage,