real-browser-mcp-server 1.3.3 → 1.4.0

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.
Files changed (118) hide show
  1. package/README.md +96 -9
  2. package/dist/lib/cjs/index.d.ts +2 -0
  3. package/dist/lib/cjs/index.d.ts.map +1 -0
  4. package/dist/lib/cjs/index.js +386 -0
  5. package/dist/lib/cjs/index.js.map +1 -0
  6. package/dist/lib/cjs/module/pageController.d.ts +2 -0
  7. package/dist/lib/cjs/module/pageController.d.ts.map +1 -0
  8. package/{lib → dist/lib}/cjs/module/pageController.js +28 -29
  9. package/dist/lib/cjs/module/pageController.js.map +1 -0
  10. package/dist/lib/cjs/module/turnstile.d.ts +2 -0
  11. package/dist/lib/cjs/module/turnstile.d.ts.map +1 -0
  12. package/{lib → dist/lib}/cjs/module/turnstile.js +24 -12
  13. package/dist/lib/cjs/module/turnstile.js.map +1 -0
  14. package/dist/src/index.d.ts +11 -0
  15. package/dist/src/index.d.ts.map +1 -0
  16. package/dist/src/index.js +118 -0
  17. package/dist/src/index.js.map +1 -0
  18. package/dist/src/mcp/handlers/browser.d.ts +30 -0
  19. package/dist/src/mcp/handlers/browser.d.ts.map +1 -0
  20. package/dist/src/mcp/handlers/browser.js +232 -0
  21. package/dist/src/mcp/handlers/browser.js.map +1 -0
  22. package/dist/src/mcp/handlers/dom.d.ts +149 -0
  23. package/dist/src/mcp/handlers/dom.d.ts.map +1 -0
  24. package/dist/src/mcp/handlers/dom.js +577 -0
  25. package/dist/src/mcp/handlers/dom.js.map +1 -0
  26. package/dist/src/mcp/handlers/extract.d.ts +59 -0
  27. package/dist/src/mcp/handlers/extract.d.ts.map +1 -0
  28. package/dist/src/mcp/handlers/extract.js +455 -0
  29. package/dist/src/mcp/handlers/extract.js.map +1 -0
  30. package/dist/src/mcp/handlers/form-handlers.d.ts +9 -0
  31. package/dist/src/mcp/handlers/form-handlers.d.ts.map +1 -0
  32. package/dist/src/mcp/handlers/form-handlers.js +56 -0
  33. package/dist/src/mcp/handlers/form-handlers.js.map +1 -0
  34. package/dist/src/mcp/handlers/helpers.d.ts +47 -0
  35. package/dist/src/mcp/handlers/helpers.d.ts.map +1 -0
  36. package/dist/src/mcp/handlers/helpers.js +515 -0
  37. package/dist/src/mcp/handlers/helpers.js.map +1 -0
  38. package/dist/src/mcp/handlers/index.d.ts +6 -0
  39. package/dist/src/mcp/handlers/index.d.ts.map +1 -0
  40. package/dist/src/mcp/handlers/index.js +61 -0
  41. package/dist/src/mcp/handlers/index.js.map +1 -0
  42. package/dist/src/mcp/handlers/media-handlers.d.ts +10 -0
  43. package/dist/src/mcp/handlers/media-handlers.d.ts.map +1 -0
  44. package/dist/src/mcp/handlers/media-handlers.js +535 -0
  45. package/dist/src/mcp/handlers/media-handlers.js.map +1 -0
  46. package/dist/src/mcp/handlers/network.d.ts +147 -0
  47. package/dist/src/mcp/handlers/network.d.ts.map +1 -0
  48. package/dist/src/mcp/handlers/network.js +1135 -0
  49. package/dist/src/mcp/handlers/network.js.map +1 -0
  50. package/dist/src/mcp/handlers/state.d.ts +34 -0
  51. package/dist/src/mcp/handlers/state.d.ts.map +1 -0
  52. package/dist/src/mcp/handlers/state.js +226 -0
  53. package/dist/src/mcp/handlers/state.js.map +1 -0
  54. package/dist/src/mcp/handlers/utility-handlers.d.ts +167 -0
  55. package/dist/src/mcp/handlers/utility-handlers.d.ts.map +1 -0
  56. package/dist/src/mcp/handlers/utility-handlers.js +280 -0
  57. package/dist/src/mcp/handlers/utility-handlers.js.map +1 -0
  58. package/dist/src/mcp/handlers/vision.d.ts +127 -0
  59. package/dist/src/mcp/handlers/vision.d.ts.map +1 -0
  60. package/dist/src/mcp/handlers/vision.js +549 -0
  61. package/dist/src/mcp/handlers/vision.js.map +1 -0
  62. package/dist/src/mcp/index.d.ts +3 -0
  63. package/dist/src/mcp/index.d.ts.map +1 -0
  64. package/dist/src/mcp/index.js +166 -0
  65. package/dist/src/mcp/index.js.map +1 -0
  66. package/dist/src/mcp/server.d.ts +2 -0
  67. package/dist/src/mcp/server.d.ts.map +1 -0
  68. package/dist/src/mcp/server.js +117 -0
  69. package/dist/src/mcp/server.js.map +1 -0
  70. package/dist/src/mcp/tools.d.ts +8 -0
  71. package/dist/src/mcp/tools.d.ts.map +1 -0
  72. package/{src → dist/src}/mcp/tools.js +12 -11
  73. package/dist/src/mcp/tools.js.map +1 -0
  74. package/dist/src/shared/cache-manager.d.ts +80 -0
  75. package/dist/src/shared/cache-manager.d.ts.map +1 -0
  76. package/dist/src/shared/cache-manager.js +221 -0
  77. package/dist/src/shared/cache-manager.js.map +1 -0
  78. package/dist/src/shared/tools.d.ts +2 -0
  79. package/dist/src/shared/tools.d.ts.map +1 -0
  80. package/dist/src/shared/tools.js +606 -0
  81. package/dist/src/shared/tools.js.map +1 -0
  82. package/dist/src/types.d.ts +376 -0
  83. package/dist/src/types.d.ts.map +1 -0
  84. package/dist/src/types.js +9 -0
  85. package/dist/src/types.js.map +1 -0
  86. package/dist/test/cjs/test.d.ts +11 -0
  87. package/dist/test/cjs/test.d.ts.map +1 -0
  88. package/dist/test/cjs/test.js +289 -0
  89. package/dist/test/cjs/test.js.map +1 -0
  90. package/dist/test/mcp/smoke-test.d.ts +29 -0
  91. package/dist/test/mcp/smoke-test.d.ts.map +1 -0
  92. package/dist/test/mcp/smoke-test.js +132 -0
  93. package/dist/test/mcp/smoke-test.js.map +1 -0
  94. package/lib/esm/index.mjs +230 -154
  95. package/lib/esm/module/pageController.mjs +21 -18
  96. package/lib/esm/module/turnstile.mjs +7 -0
  97. package/package.json +25 -16
  98. package/typings.d.ts +7 -1
  99. package/.github/ISSUE_TEMPLATE/general_issue.yaml +0 -58
  100. package/.github/SETUP.md +0 -111
  101. package/.github/workflows/publish.yml +0 -135
  102. package/Dockerfile +0 -79
  103. package/lib/cjs/adblocker.bin +0 -0
  104. package/lib/cjs/index.js +0 -321
  105. package/src/ai/action-parser.js +0 -274
  106. package/src/ai/core.js +0 -378
  107. package/src/ai/element-finder.js +0 -466
  108. package/src/ai/index.js +0 -82
  109. package/src/ai/page-analyzer.js +0 -304
  110. package/src/ai/selector-healer.js +0 -236
  111. package/src/index.js +0 -121
  112. package/src/mcp/handlers.js +0 -5071
  113. package/src/mcp/index.js +0 -190
  114. package/src/mcp/server.js +0 -144
  115. package/src/shared/tools.js +0 -618
  116. package/test/cjs/test.js +0 -259
  117. package/test/esm/package.json +0 -13
  118. package/test/esm/test.js +0 -226
package/lib/esm/index.mjs CHANGED
@@ -3,9 +3,9 @@ import { createCursor } from "ghost-cursor-patchright";
3
3
  import { PlaywrightBlocker } from "@ghostery/adblocker-playwright";
4
4
  import { pageController } from "./module/pageController.mjs";
5
5
  import { fileURLToPath } from 'url';
6
- import Xvfb from "xvfb";
7
6
  import * as fs from 'fs';
8
7
  import * as path from 'path';
8
+ import { execSync } from 'child_process';
9
9
 
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
@@ -71,13 +71,28 @@ function loadEnvFile() {
71
71
  loadEnvFile();
72
72
 
73
73
  function getDefaultHeadless() {
74
- const envHeadless = (process.env.HEADLESS || '').toLowerCase();
75
- return envHeadless === 'true';
74
+ const envHeadless = process.env.HEADLESS;
75
+ if (envHeadless !== undefined && envHeadless !== null && envHeadless !== '') {
76
+ const value = envHeadless.toLowerCase().trim();
77
+ return value === 'true' || value === '1' || value === 'yes';
78
+ }
79
+ // Auto-detect CI environments
80
+ if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.TRAVIS || process.env.CIRCLECI) {
81
+ return true;
82
+ }
83
+ // Auto-detect headless Linux environments without X11 or Wayland
84
+ if (process.platform === 'linux') {
85
+ const hasDisplay = process.env.DISPLAY || process.env.WAYLAND_DISPLAY;
86
+ if (!hasDisplay) {
87
+ return true;
88
+ }
89
+ }
90
+ return false;
76
91
  }
77
92
 
78
- function applyBrowserShims(browser, page) {
79
- if (page._shimsApplied) return page;
80
- page._shimsApplied = true;
93
+ function setupRealPage(browser, page) {
94
+ if (page._setupApplied) return page;
95
+ page._setupApplied = true;
81
96
 
82
97
  // Enable ad blocker
83
98
  if (adBlockerInstance) {
@@ -90,115 +105,164 @@ function applyBrowserShims(browser, page) {
90
105
  });
91
106
  }
92
107
 
93
- // Cookie shims (Compatibility adapter for Playwright context)
94
- if (!page.cookies) {
95
- page.cookies = async (urls) => {
96
- return await page.context().cookies(urls ? (Array.isArray(urls) ? urls : [urls]) : []);
97
- };
98
- }
99
-
100
- if (!page.setCookie) {
101
- page.setCookie = async (...cookies) => {
102
- const formatted = cookies.map(c => ({
103
- name: c.name,
104
- value: c.value,
105
- domain: c.domain || new URL(page.url()).hostname,
106
- path: c.path || '/',
107
- expires: c.expires || undefined,
108
- httpOnly: c.httpOnly,
109
- secure: c.secure,
110
- sameSite: c.sameSite
111
- }));
112
- await page.context().addCookies(formatted);
113
- };
114
- }
108
+ // Human-like smooth scrolling with 60FPS Cubic Ease-Out physics
109
+ page.realScroll = async (deltaY, duration = 600) => {
110
+ try {
111
+ const stepDelay = 15; // ~60 FPS
112
+ const steps = Math.max(10, Math.floor(duration / stepDelay));
113
+ const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
114
+ let currentScroll = 0;
115
+ for (let i = 1; i <= steps; i++) {
116
+ const t = i / steps;
117
+ const targetScroll = deltaY * easeOutCubic(t);
118
+ const diff = targetScroll - currentScroll;
119
+ await page.mouse.wheel(0, diff);
120
+ currentScroll = targetScroll;
121
+ await new Promise(r => setTimeout(r, stepDelay));
122
+ }
123
+ } catch (e) {
124
+ // Fallback to native window scroll in case of wheel errors
125
+ try {
126
+ await page.evaluate((y) => window.scrollBy({ top: y, behavior: 'smooth' }), deltaY);
127
+ } catch (_) {}
128
+ }
129
+ };
115
130
 
116
- if (!page.deleteCookie) {
117
- page.deleteCookie = async (...cookies) => {
118
- await page.context().clearCookies();
131
+ // Ghost Cursor integration - Bézier curve human-like mouse movement
132
+ try {
133
+ const cursor = createCursor(page);
134
+ page.realCursor = {
135
+ move: async (selector, options = {}) => {
136
+ try {
137
+ await cursor.actions.move(selector, options);
138
+ } catch (e) {
139
+ // Fallback to native hover if ghost-cursor fails
140
+ try { await page.hover(selector); } catch (_) {}
141
+ }
142
+ }
119
143
  };
120
- }
121
-
122
- // Navigation shims
123
- if (!page.waitForNetworkIdle) {
124
- page.waitForNetworkIdle = async (options = {}) => {
144
+ page.realClick = async (selector, options = {}) => {
125
145
  try {
126
- await page.waitForLoadState('networkidle', { timeout: options.timeout });
146
+ await cursor.actions.click({ target: selector, ...options });
127
147
  } catch (e) {
128
- // Ignore networkidle timeouts if they are non-fatal
148
+ // Fallback to native click if ghost-cursor fails
149
+ await page.click(selector, options);
129
150
  }
130
151
  };
152
+ } catch (e) {
153
+ // Fallback if ghost-cursor-patchright fails to initialize
154
+ if (!page.realClick) {
155
+ page.realClick = async (selector, options) => {
156
+ await page.click(selector, options);
157
+ };
158
+ }
159
+ if (!page.realCursor) {
160
+ page.realCursor = {
161
+ move: async (selector) => {
162
+ try { await page.hover(selector); } catch (_) {}
163
+ }
164
+ };
165
+ }
131
166
  }
132
167
 
133
- if (!page.evaluateOnNewDocument) {
134
- page.evaluateOnNewDocument = async (fn, ...args) => {
135
- return await page.addInitScript(fn, ...args);
136
- };
168
+ return page;
169
+
170
+ }
171
+
172
+ function getBraveExecutablePath() {
173
+ if (process.env.BRAVE_PATH && fs.existsSync(process.env.BRAVE_PATH)) {
174
+ return process.env.BRAVE_PATH;
137
175
  }
138
176
 
139
- // Ghost Cursor integration via ghost-cursor-patchright
140
- const cursorPromise = createCursor(page).catch(err => {
141
- console.error("Failed to create cursor:", err);
142
- return null;
143
- });
177
+ const platform = process.platform;
178
+
179
+ // Try automatic scanning via CLI / registry query
180
+ if (platform === 'win32') {
181
+ const regQueries = [
182
+ 'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\brave.exe" /ve',
183
+ 'reg query "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\brave.exe" /ve',
184
+ 'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\StartMenuInternet\\Brave-Browser\\shell\\open\\command" /ve'
185
+ ];
144
186
 
145
- page.realCursor = {
146
- move: async (selector, options = {}) => {
187
+ for (const cmd of regQueries) {
147
188
  try {
148
- const cursor = await cursorPromise;
149
- if (cursor) {
150
- await cursor.move(selector, options);
151
- } else {
152
- await page.hover(selector, options);
189
+ const output = execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
190
+ const match = output.match(/REG_SZ\s+(.*)/);
191
+ if (match && match[1]) {
192
+ let p = match[1].trim().replace(/^"|"$/g, '');
193
+ if (!p.toLowerCase().endsWith('.exe')) {
194
+ const exeIndex = p.toLowerCase().indexOf('.exe');
195
+ if (exeIndex !== -1) {
196
+ p = p.substring(0, exeIndex + 4).replace(/^"|"$/g, '');
197
+ }
198
+ }
199
+ if (fs.existsSync(p)) return p;
153
200
  }
154
- } catch (e) {
155
- // Silently fallback if element is not found
156
- try {
157
- await page.hover(selector, options);
158
- } catch (err) {}
159
- }
160
- }
161
- };
162
- page.realClick = async (selector, options = {}) => {
163
- try {
164
- const cursor = await cursorPromise;
165
- if (cursor) {
166
- await cursor.click(selector, options);
167
- } else {
168
- await page.click(selector, options);
169
- }
170
- } catch (e) {
171
- await page.click(selector, options);
201
+ } catch (e) {}
172
202
  }
173
- };
174
203
 
175
- // Mouse wheel shim (Adapter for Playwright positional args)
176
- if (page.mouse && page.mouse.wheel) {
177
- const originalWheel = page.mouse.wheel.bind(page.mouse);
178
- page.mouse.wheel = async (x, y) => {
179
- if (typeof x === 'object' && x !== null) {
180
- const deltaX = x.deltaX || 0;
181
- const deltaY = x.deltaY || 0;
182
- return await originalWheel(deltaX, deltaY);
204
+ try {
205
+ const output = execSync('where brave.exe', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim().split('\r\n')[0];
206
+ if (output && fs.existsSync(output)) return output;
207
+ } catch (e) {}
208
+ } else if (platform === 'darwin') {
209
+ try {
210
+ const output = execSync('mdfind "kMDItemCFBundleIdentifier == \'com.brave.Browser\'"', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim().split('\n')[0];
211
+ if (output) {
212
+ const p = path.join(output, 'Contents', 'MacOS', 'Brave Browser');
213
+ if (fs.existsSync(p)) return p;
183
214
  }
184
- return await originalWheel(x, y);
185
- };
215
+ } catch (e) {}
216
+ } else {
217
+ try {
218
+ const output = execSync('which brave-browser || which brave', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
219
+ if (output && fs.existsSync(output)) return output;
220
+ } catch (e) {}
186
221
  }
187
222
 
188
- // Browser shims
189
- if (!browser.process) {
190
- browser.process = () => ({
191
- pid: browser._childProcess?.pid || 1337
192
- });
223
+ // Fallback to hardcoded common paths
224
+ let paths = [];
225
+ if (platform === 'win32') {
226
+ paths = [
227
+ path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
228
+ path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
229
+ path.join(process.env.LOCALAPPDATA || '', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe')
230
+ ].filter(p => p);
231
+ } else if (platform === 'darwin') {
232
+ paths = [
233
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'
234
+ ];
235
+ } else {
236
+ paths = [
237
+ '/usr/bin/brave-browser',
238
+ '/usr/bin/brave',
239
+ '/usr/bin/brave-browser-stable',
240
+ '/usr/bin/brave-browser-beta',
241
+ '/usr/bin/brave-browser-nightly',
242
+ '/usr/local/bin/brave-browser',
243
+ '/usr/local/bin/brave'
244
+ ];
193
245
  }
194
246
 
195
- if (!browser.pages) {
196
- browser.pages = async () => {
197
- return browser.contexts().flatMap(c => c.pages());
198
- };
247
+ for (const p of paths) {
248
+ if (p && fs.existsSync(p)) {
249
+ return p;
250
+ }
199
251
  }
200
252
 
201
- return page;
253
+ return null;
254
+ }
255
+
256
+ async function applyUserAgentOverride(page, userAgent, userAgentMetadata) {
257
+ try {
258
+ const client = await page.context().newCDPSession(page);
259
+ await client.send('Emulation.setUserAgentOverride', {
260
+ userAgent: userAgent,
261
+ userAgentMetadata: userAgentMetadata
262
+ });
263
+ } catch (e) {
264
+ // Ignore errors
265
+ }
202
266
  }
203
267
 
204
268
  export async function connect({
@@ -206,21 +270,8 @@ export async function connect({
206
270
  headless = getDefaultHeadless(),
207
271
  proxy = {},
208
272
  turnstile = false,
209
- disableXvfb = false,
273
+ executablePath = undefined,
210
274
  } = {}) {
211
- let xvfbInstance = null;
212
- if (process.platform === 'linux' && !process.env.DISPLAY && !disableXvfb) {
213
- try {
214
- xvfbInstance = new Xvfb({
215
- silent: true,
216
- xvfb_args: ['-screen', '0', '1280x1024x24', '-ac']
217
- });
218
- xvfbInstance.startSync();
219
- } catch (err) {
220
- console.error('[xvfb] Failed to start Xvfb server automatically:', err.message);
221
- }
222
- }
223
-
224
275
  let playwrightProxy = undefined;
225
276
  if (proxy && proxy.host && proxy.port) {
226
277
  playwrightProxy = {
@@ -232,60 +283,82 @@ export async function connect({
232
283
  }
233
284
  }
234
285
 
286
+ // 1. Launch a temporary browser to retrieve the native user agent and properties
287
+ const tempBrowser = await chromium.launch({
288
+ headless: true,
289
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
290
+ ...(executablePath ? { executablePath } : {}),
291
+ });
292
+ const tempContext = await tempBrowser.newContext();
293
+ const tempPage = await tempContext.newPage();
294
+ let nativeUa = '';
295
+ let isBrave = false;
296
+ try {
297
+ nativeUa = await tempPage.evaluate(() => navigator.userAgent);
298
+ isBrave = await tempPage.evaluate(() => typeof navigator.brave !== 'undefined');
299
+ } catch (e) {
300
+ nativeUa = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36';
301
+ isBrave = executablePath && executablePath.toLowerCase().includes('brave');
302
+ }
303
+ await tempBrowser.close();
304
+
305
+ let modifiedUa = nativeUa.replace(/HeadlessChrome\//g, 'Chrome/');
306
+ const chromeVersionMatch = modifiedUa.match(/Chrome\/([\d.]+)/);
307
+ const chromeVersion = chromeVersionMatch ? chromeVersionMatch[1] : '148.0.0.0';
308
+ const majorVersion = chromeVersion.split('.')[0];
309
+
310
+ const brands = [
311
+ { brand: 'Chromium', version: majorVersion },
312
+ { brand: 'Not/A)Brand', version: '99' }
313
+ ];
314
+ if (isBrave) {
315
+ brands.unshift({ brand: 'Brave', version: majorVersion });
316
+ } else {
317
+ brands.unshift({ brand: 'Google Chrome', version: majorVersion });
318
+ }
319
+
320
+ let platformName = 'Windows';
321
+ if (nativeUa.includes('Macintosh') || nativeUa.includes('Mac OS X')) {
322
+ platformName = 'macOS';
323
+ } else if (nativeUa.includes('Linux')) {
324
+ platformName = 'Linux';
325
+ }
326
+
327
+ const userAgentMetadata = {
328
+ brands: brands,
329
+ mobile: false,
330
+ platform: platformName,
331
+ platformVersion: platformName === 'macOS' ? '14.0.0' : platformName === 'Linux' ? '6.0.0' : '10.0.0',
332
+ architecture: 'x86',
333
+ model: '',
334
+ bitness: '64',
335
+ wow64: false
336
+ };
337
+
235
338
  const chromiumArgs = [
339
+ `--user-agent=${modifiedUa}`,
236
340
  '--disable-blink-features=AutomationControlled',
237
341
  '--no-sandbox',
238
342
  '--disable-setuid-sandbox',
239
- '--window-size=1920,1080',
240
343
  ...args
241
344
  ];
242
345
 
243
- if (headless) {
244
- chromiumArgs.push(
245
- '--disable-gpu',
246
- '--hide-scrollbars',
247
- '--mute-audio'
248
- );
249
- }
250
-
251
- let browser;
252
- try {
253
- // Try launching using the system's real installed Google Chrome first for maximum stealth/fingerprint integrity
254
- browser = await chromium.launch({
255
- headless,
256
- args: chromiumArgs,
257
- proxy: playwrightProxy,
258
- channel: 'chrome'
259
- });
260
- console.error('[Launch] Successfully launched using system Google Chrome channel');
261
- } catch (e) {
262
- // Fallback to pre-packaged Chromium if Chrome is not installed
263
- browser = await chromium.launch({
264
- headless,
265
- args: chromiumArgs,
266
- proxy: playwrightProxy
267
- });
268
- console.error('[Launch] Google Chrome channel not available, falling back to pre-packaged Chromium');
346
+ // If headless is true, we run with headless: false but pass '--headless=new' to args.
347
+ // This triggers Chromium's modern undetected headless mode instead of Playwright's default old headless shell.
348
+ let launchHeadless = headless;
349
+ if (headless === true) {
350
+ launchHeadless = false;
351
+ if (!chromiumArgs.includes('--headless=new')) {
352
+ chromiumArgs.push('--headless=new');
353
+ }
269
354
  }
270
355
 
271
- if (xvfbInstance) {
272
- browser.on('disconnected', () => {
273
- try {
274
- xvfbInstance.stopSync();
275
- } catch (e) {
276
- console.error('[xvfb] Failed to stop Xvfb server:', e.message);
277
- }
278
- });
279
- const originalClose = browser.close.bind(browser);
280
- browser.close = async () => {
281
- await originalClose();
282
- try {
283
- xvfbInstance.stopSync();
284
- } catch (e) {
285
- console.error('[xvfb] Failed to stop Xvfb server during close:', e.message);
286
- }
287
- };
288
- }
356
+ const browser = await chromium.launch({
357
+ headless: launchHeadless,
358
+ args: chromiumArgs,
359
+ proxy: playwrightProxy,
360
+ ...(executablePath ? { executablePath } : {}),
361
+ });
289
362
 
290
363
  // Ensure ad blocker is ready
291
364
  await getAdBlocker();
@@ -296,7 +369,9 @@ export async function connect({
296
369
 
297
370
  let page = await context.newPage();
298
371
 
299
- applyBrowserShims(browser, page);
372
+ await applyUserAgentOverride(page, modifiedUa, userAgentMetadata);
373
+
374
+ setupRealPage(browser, page);
300
375
 
301
376
  page = await pageController({
302
377
  browser,
@@ -306,7 +381,8 @@ export async function connect({
306
381
  });
307
382
 
308
383
  context.on('page', async (newPage) => {
309
- applyBrowserShims(browser, newPage);
384
+ await applyUserAgentOverride(newPage, modifiedUa, userAgentMetadata);
385
+ setupRealPage(browser, newPage);
310
386
  await pageController({
311
387
  browser,
312
388
  page: newPage,
@@ -24,26 +24,29 @@ export async function pageController({ browser, page, proxy, turnstile }) {
24
24
 
25
25
  // === POPUP AD BLOCKING ===
26
26
  const context = page.context();
27
- context.on('page', async (newPage) => {
28
- try {
29
- const opener = await newPage.opener();
30
- if (opener) {
31
- const url = newPage.url();
32
- const isAdPopup = url === 'about:blank' ||
33
- url.includes('ad') ||
34
- url.includes('pop') ||
35
- url.includes('click') ||
36
- url.includes('redirect') ||
37
- url.includes('track');
38
- if (isAdPopup) {
39
- await newPage.close().catch(() => { });
40
- console.error('[popup-blocker] Blocked popup ad:', url.substring(0, 50));
27
+ if (!context._popupBlockerApplied) {
28
+ context._popupBlockerApplied = true;
29
+ context.on('page', async (newPage) => {
30
+ try {
31
+ const opener = await newPage.opener();
32
+ if (opener) {
33
+ const url = newPage.url();
34
+ const isAdPopup = url === 'about:blank' ||
35
+ url.includes('ad') ||
36
+ url.includes('pop') ||
37
+ url.includes('click') ||
38
+ url.includes('redirect') ||
39
+ url.includes('track');
40
+ if (isAdPopup) {
41
+ await newPage.close().catch(() => { });
42
+ console.error('[popup-blocker] Blocked popup ad:', url.substring(0, 50));
43
+ }
41
44
  }
45
+ } catch (e) {
46
+ // Ignore errors
42
47
  }
43
- } catch (e) {
44
- // Ignore errors
45
- }
46
- });
48
+ });
49
+ }
47
50
 
48
51
  // NOTE: JS stealth overrides are commented out because Patchright natively handles automation hiding.
49
52
  // Manual JS overrides trigger Pixelscan fingerprint masking detectors.
@@ -2,6 +2,13 @@ export const checkTurnstile = async ({ page }) => {
2
2
  try {
3
3
  const elements = await page.locator('[name="cf-turnstile-response"]').all();
4
4
  if (elements.length <= 0) {
5
+ const isChallenge = await page.evaluate(() => {
6
+ return document.title.includes('Just a moment') ||
7
+ document.querySelector('#challenge-stage') !== null ||
8
+ document.querySelector('.cf-turnstile') !== null;
9
+ });
10
+ if (!isChallenge) return false;
11
+
5
12
  const coordinates = await page.evaluate(() => {
6
13
  let coordinates = [];
7
14
  document.querySelectorAll('div').forEach(item => {
package/package.json CHANGED
@@ -1,35 +1,41 @@
1
1
  {
2
2
  "name": "real-browser-mcp-server",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "MCP Server for Real Browser - Patchright (undetected Playwright fork) with Stealth Mode, Ad Blocker, and Turnstile Auto-Solver for undetectable web automation.",
5
- "main": "lib/cjs/index.js",
5
+ "main": "dist/lib/cjs/index.js",
6
6
  "module": "lib/esm/index.mjs",
7
7
  "type": "commonjs",
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./typings.d.ts",
11
11
  "import": "./lib/esm/index.mjs",
12
- "require": "./lib/cjs/index.js"
12
+ "require": "./dist/lib/cjs/index.js"
13
13
  }
14
14
  },
15
+ "files": [
16
+ "dist",
17
+ "lib/esm",
18
+ "typings.d.ts"
19
+ ],
15
20
  "typings": "typings.d.ts",
16
21
  "engines": {
17
22
  "node": ">=18.0.0"
18
23
  },
19
24
  "bin": {
20
- "real-browser-mcp": "./src/index.js"
25
+ "real-browser-mcp": "./dist/src/index.js"
21
26
  },
22
27
  "scripts": {
23
- "start": "node src/index.js",
24
- "dev": "node src/index.js",
25
- "mcp": "node src/index.js mcp",
26
- "mcp:verbose": "node src/index.js mcp --verbose",
27
- "list": "node src/index.js --list",
28
+ "prepublishOnly": "npm run build",
29
+ "build": "tsc",
30
+ "start": "node dist/src/index.js",
31
+ "dev": "npm run build && node dist/src/index.js",
32
+ "mcp": "node dist/src/index.js mcp",
33
+ "mcp:verbose": "node dist/src/index.js mcp --verbose",
34
+ "list": "node dist/src/index.js --list",
28
35
  "test": "npm run cjs_test && npm run esm_test",
29
- "esm_test": "node ./test/esm/test.js",
30
- "cjs_test": "node ./test/cjs/test.js",
31
- "build": "echo 'No build step required for this package.'",
32
- "postinstall": "npx patchright install chromium"
36
+ "esm_test": "node ./test/esm/test.mjs",
37
+ "cjs_test": "node ./dist/test/cjs/test.js",
38
+ "mcp_test": "node ./dist/test/mcp/smoke-test.js"
33
39
  },
34
40
  "keywords": [
35
41
  "mcp-server",
@@ -45,8 +51,11 @@
45
51
  "dependencies": {
46
52
  "@ghostery/adblocker-playwright": "latest",
47
53
  "@modelcontextprotocol/sdk": "latest",
48
- "patchright": "latest",
49
- "ghost-cursor-patchright": "file:../ghost-cursor",
50
- "xvfb": "latest"
54
+ "ghost-cursor-patchright": "^1.0.2",
55
+ "patchright": "latest"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^25.9.2",
59
+ "typescript": "^6.0.3"
51
60
  }
52
61
  }
package/typings.d.ts CHANGED
@@ -24,8 +24,9 @@ declare module "real-browser-mcp-server" {
24
24
  turnstile?: boolean;
25
25
  connectOption?: any;
26
26
  disableXvfb?: boolean;
27
- plugins?: any[];
28
27
  ignoreAllFlags?: boolean;
28
+ /** Path to the browser executable (defaults to auto-detected Brave browser or falls back to Chromium) */
29
+ executablePath?: string;
29
30
  /** Enable blocker on all pages (default: true) */
30
31
  enableBlocker?: boolean;
31
32
  /** Blocker configuration options */
@@ -77,3 +78,8 @@ declare module "real-browser-mcp-server" {
77
78
  shouldBlock(url: string): boolean;
78
79
  }
79
80
  }
81
+
82
+ declare module "real-browser-mcp-server" {
83
+ export * from "real-browser-mcp-server";
84
+ }
85
+