real-browser-mcp-server 1.1.1 → 1.1.2
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 +0 -1
- package/README.md +62 -2
- package/lib/cjs/index.js +231 -84
- package/lib/esm/index.mjs +232 -79
- package/package.json +5 -6
- package/src/ai/core.js +1 -1
- package/src/index.js +8 -1
- package/src/mcp/handlers.js +33 -17
- package/src/mcp/server.js +1 -1
- package/test/cjs/test.js +143 -66
- package/test/esm/{test.js → test.mjs} +131 -58
- package/typings.d.ts +12 -6
- package/test/esm/package.json +0 -13
- package/test/esm/test_option2.js +0 -46
- package/test/esm/test_playwright_ghost.js +0 -30
package/lib/esm/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
const { chromium } = playwright;
|
|
1
|
+
import { chromium } from "patchright";
|
|
2
|
+
import { createCursor } from "ghost-cursor-patchright";
|
|
4
3
|
import { PlaywrightBlocker } from "@ghostery/adblocker-playwright";
|
|
5
4
|
import { pageController } from "./module/pageController.mjs";
|
|
6
5
|
import { fileURLToPath } from 'url';
|
|
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 =
|
|
75
|
-
|
|
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
|
|
79
|
-
if (page.
|
|
80
|
-
page.
|
|
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,93 +105,164 @@ function applyPuppeteerShims(browser, page) {
|
|
|
90
105
|
});
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
146
|
+
await cursor.actions.click({ target: selector, ...options });
|
|
127
147
|
} catch (e) {
|
|
128
|
-
//
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
];
|
|
186
|
+
|
|
187
|
+
for (const cmd of regQueries) {
|
|
142
188
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
200
|
+
}
|
|
201
|
+
} catch (e) {}
|
|
147
202
|
}
|
|
148
|
-
};
|
|
149
|
-
page.realClick = async (selector, options = {}) => {
|
|
150
|
-
await page.click(selector, options);
|
|
151
|
-
};
|
|
152
203
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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;
|
|
161
214
|
}
|
|
162
|
-
|
|
163
|
-
|
|
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) {}
|
|
164
221
|
}
|
|
165
222
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
];
|
|
171
245
|
}
|
|
172
246
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return
|
|
176
|
-
}
|
|
247
|
+
for (const p of paths) {
|
|
248
|
+
if (p && fs.existsSync(p)) {
|
|
249
|
+
return p;
|
|
250
|
+
}
|
|
177
251
|
}
|
|
178
252
|
|
|
179
|
-
return
|
|
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
|
+
}
|
|
180
266
|
}
|
|
181
267
|
|
|
182
268
|
export async function connect({
|
|
@@ -184,6 +270,7 @@ export async function connect({
|
|
|
184
270
|
headless = getDefaultHeadless(),
|
|
185
271
|
proxy = {},
|
|
186
272
|
turnstile = false,
|
|
273
|
+
executablePath = undefined,
|
|
187
274
|
} = {}) {
|
|
188
275
|
let playwrightProxy = undefined;
|
|
189
276
|
if (proxy && proxy.host && proxy.port) {
|
|
@@ -196,18 +283,81 @@ export async function connect({
|
|
|
196
283
|
}
|
|
197
284
|
}
|
|
198
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
|
+
|
|
199
338
|
const chromiumArgs = [
|
|
339
|
+
`--user-agent=${modifiedUa}`,
|
|
200
340
|
'--disable-blink-features=AutomationControlled',
|
|
201
341
|
'--no-sandbox',
|
|
202
342
|
'--disable-setuid-sandbox',
|
|
203
343
|
...args
|
|
204
344
|
];
|
|
205
345
|
|
|
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
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
206
356
|
const browser = await chromium.launch({
|
|
207
|
-
headless,
|
|
357
|
+
headless: launchHeadless,
|
|
208
358
|
args: chromiumArgs,
|
|
209
359
|
proxy: playwrightProxy,
|
|
210
|
-
|
|
360
|
+
...(executablePath ? { executablePath } : {}),
|
|
211
361
|
});
|
|
212
362
|
|
|
213
363
|
// Ensure ad blocker is ready
|
|
@@ -219,7 +369,9 @@ export async function connect({
|
|
|
219
369
|
|
|
220
370
|
let page = await context.newPage();
|
|
221
371
|
|
|
222
|
-
|
|
372
|
+
await applyUserAgentOverride(page, modifiedUa, userAgentMetadata);
|
|
373
|
+
|
|
374
|
+
setupRealPage(browser, page);
|
|
223
375
|
|
|
224
376
|
page = await pageController({
|
|
225
377
|
browser,
|
|
@@ -229,7 +381,8 @@ export async function connect({
|
|
|
229
381
|
});
|
|
230
382
|
|
|
231
383
|
context.on('page', async (newPage) => {
|
|
232
|
-
|
|
384
|
+
await applyUserAgentOverride(newPage, modifiedUa, userAgentMetadata);
|
|
385
|
+
setupRealPage(browser, newPage);
|
|
233
386
|
await pageController({
|
|
234
387
|
browser,
|
|
235
388
|
page: newPage,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "real-browser-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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
5
|
"main": "lib/cjs/index.js",
|
|
6
6
|
"module": "lib/esm/index.mjs",
|
|
@@ -26,9 +26,8 @@
|
|
|
26
26
|
"mcp:verbose": "node src/index.js mcp --verbose",
|
|
27
27
|
"list": "node src/index.js --list",
|
|
28
28
|
"test": "npm run cjs_test && npm run esm_test",
|
|
29
|
-
"esm_test": "node ./test/esm/test.
|
|
30
|
-
"cjs_test": "node ./test/cjs/test.js"
|
|
31
|
-
"build": "echo 'No build step required for this package.'"
|
|
29
|
+
"esm_test": "node ./test/esm/test.mjs",
|
|
30
|
+
"cjs_test": "node ./test/cjs/test.js"
|
|
32
31
|
},
|
|
33
32
|
"keywords": [
|
|
34
33
|
"mcp-server",
|
|
@@ -44,7 +43,7 @@
|
|
|
44
43
|
"dependencies": {
|
|
45
44
|
"@ghostery/adblocker-playwright": "latest",
|
|
46
45
|
"@modelcontextprotocol/sdk": "latest",
|
|
47
|
-
"patchright": "
|
|
48
|
-
"
|
|
46
|
+
"ghost-cursor-patchright": "^1.0.2",
|
|
47
|
+
"patchright": "latest"
|
|
49
48
|
}
|
|
50
49
|
}
|
package/src/ai/core.js
CHANGED
|
@@ -90,7 +90,7 @@ class AICore {
|
|
|
90
90
|
if (element) {
|
|
91
91
|
if (humanLike) {
|
|
92
92
|
try {
|
|
93
|
-
const { createCursor } = require('ghost-cursor');
|
|
93
|
+
const { createCursor } = require('ghost-cursor-patchright');
|
|
94
94
|
const cursor = createCursor(page);
|
|
95
95
|
await cursor.click(selector);
|
|
96
96
|
} catch {
|
package/src/index.js
CHANGED
|
@@ -97,7 +97,14 @@ async function main() {
|
|
|
97
97
|
process.exit(0);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
console.error('
|
|
100
|
+
console.error('');
|
|
101
|
+
console.error(`${colors.bright}${colors.cyan}╔════════════════════════════════════════════════════════════╗${colors.reset}`);
|
|
102
|
+
console.error(`${colors.bright}${colors.cyan}║${colors.reset} ${colors.bright}${colors.magenta}🦁 Real Browser - Unified Server${colors.reset} ${colors.cyan}║${colors.reset}`);
|
|
103
|
+
console.error(`${colors.bright}${colors.cyan}║${colors.reset} ${colors.dim}MCP (AI Agents) Server running on STDIO${colors.reset} ${colors.cyan}║${colors.reset}`);
|
|
104
|
+
console.error(`${colors.bright}${colors.cyan}╚════════════════════════════════════════════════════════════╝${colors.reset}`);
|
|
105
|
+
console.error('');
|
|
106
|
+
|
|
107
|
+
console.error(`${colors.bright}${colors.blue}🚀 Starting MCP Server...${colors.reset}`);
|
|
101
108
|
|
|
102
109
|
// Import and run MCP server
|
|
103
110
|
require('./mcp/index.js');
|
package/src/mcp/handlers.js
CHANGED
|
@@ -68,13 +68,25 @@ function notifyProgress(toolName, status, message, data = {}) {
|
|
|
68
68
|
function getHeadlessFromEnv() {
|
|
69
69
|
const envHeadless = process.env.HEADLESS;
|
|
70
70
|
|
|
71
|
-
if (envHeadless
|
|
72
|
-
|
|
71
|
+
if (envHeadless !== undefined && envHeadless !== null && envHeadless !== '') {
|
|
72
|
+
const value = envHeadless.toLowerCase().trim();
|
|
73
|
+
return value === 'true' || value === '1' || value === 'yes';
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
// Auto-detect CI environments
|
|
77
|
+
if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.TRAVIS || process.env.CIRCLECI) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Auto-detect headless Linux environments without X11 or Wayland
|
|
82
|
+
if (process.platform === 'linux') {
|
|
83
|
+
const hasDisplay = process.env.DISPLAY || process.env.WAYLAND_DISPLAY;
|
|
84
|
+
if (!hasDisplay) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return false;
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/**
|
|
@@ -595,9 +607,9 @@ const handlers = {
|
|
|
595
607
|
// ═══════════════════════════════════════════════════════════════
|
|
596
608
|
// INJECTED SCRIPT - Silent Handling of Popups
|
|
597
609
|
// Override window.confirm/alert to handle them inside the page context
|
|
598
|
-
// Note: Using
|
|
610
|
+
// Note: Using addInitScript (Playwright) to intercept popups early
|
|
599
611
|
// ═══════════════════════════════════════════════════════════════
|
|
600
|
-
await pageInstance.
|
|
612
|
+
await pageInstance.addInitScript(() => {
|
|
601
613
|
window.originalConfirm = window.confirm;
|
|
602
614
|
window.originalAlert = window.alert;
|
|
603
615
|
|
|
@@ -1239,7 +1251,7 @@ const handlers = {
|
|
|
1239
1251
|
notifyProgress('click', 'progress', 'Used force click (JS)');
|
|
1240
1252
|
} else if (humanLike) {
|
|
1241
1253
|
try {
|
|
1242
|
-
const { createCursor } = require('ghost-cursor');
|
|
1254
|
+
const { createCursor } = require('ghost-cursor-patchright');
|
|
1243
1255
|
const cursor = createCursor(page);
|
|
1244
1256
|
|
|
1245
1257
|
if (context !== page) {
|
|
@@ -2002,7 +2014,7 @@ const handlers = {
|
|
|
2002
2014
|
|
|
2003
2015
|
// Click submit button with human-like behavior
|
|
2004
2016
|
try {
|
|
2005
|
-
const { createCursor } = require('ghost-cursor');
|
|
2017
|
+
const { createCursor } = require('ghost-cursor-patchright');
|
|
2006
2018
|
const cursor = createCursor(page);
|
|
2007
2019
|
await cursor.click(submitSelector);
|
|
2008
2020
|
} catch (e) {
|
|
@@ -2049,10 +2061,14 @@ const handlers = {
|
|
|
2049
2061
|
|
|
2050
2062
|
notifyProgress('random_scroll', 'started', `Scrolling ${scrollDirection} ${scrollAmount}px`);
|
|
2051
2063
|
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
}
|
|
2064
|
+
const y = scrollDirection === 'down' ? scrollAmount : -scrollAmount;
|
|
2065
|
+
if (smooth && page.realScroll) {
|
|
2066
|
+
await page.realScroll(y, 600);
|
|
2067
|
+
} else {
|
|
2068
|
+
await page.evaluate(({ y, smooth }) => {
|
|
2069
|
+
window.scrollBy({ top: y, behavior: smooth ? 'smooth' : 'auto' });
|
|
2070
|
+
}, { y, smooth });
|
|
2071
|
+
}
|
|
2056
2072
|
|
|
2057
2073
|
notifyProgress('random_scroll', 'completed', `Scrolled ${scrollDirection} ${scrollAmount}px`, { direction: scrollDirection, amount: scrollAmount });
|
|
2058
2074
|
|
|
@@ -2448,7 +2464,7 @@ const handlers = {
|
|
|
2448
2464
|
// ====== FEATURE 2: Pre-page-load Runtime API Interception ======
|
|
2449
2465
|
// Inject BEFORE any JS runs — catches calls from obfuscated/webpack code
|
|
2450
2466
|
try {
|
|
2451
|
-
await page.
|
|
2467
|
+
await page.addInitScript(() => {
|
|
2452
2468
|
window.__interceptedApis = [];
|
|
2453
2469
|
window.__wsMessages = [];
|
|
2454
2470
|
|
|
@@ -2552,7 +2568,7 @@ const handlers = {
|
|
|
2552
2568
|
window.WebSocket.CLOSING = OrigWS.CLOSING;
|
|
2553
2569
|
window.WebSocket.CLOSED = OrigWS.CLOSED;
|
|
2554
2570
|
});
|
|
2555
|
-
} catch (e) { /*
|
|
2571
|
+
} catch (e) { /* addInitScript may fail on already-loaded pages, that's OK */ }
|
|
2556
2572
|
|
|
2557
2573
|
// Request handler
|
|
2558
2574
|
page.on('request', req => {
|
|
@@ -4768,7 +4784,7 @@ const handlers = {
|
|
|
4768
4784
|
}, identity, String(value));
|
|
4769
4785
|
} else {
|
|
4770
4786
|
// Smart Type
|
|
4771
|
-
const { createCursor } = require('ghost-cursor');
|
|
4787
|
+
const { createCursor } = require('ghost-cursor-patchright');
|
|
4772
4788
|
const cursor = createCursor(page);
|
|
4773
4789
|
|
|
4774
4790
|
// Click center of element
|
|
@@ -4835,7 +4851,7 @@ const handlers = {
|
|
|
4835
4851
|
});
|
|
4836
4852
|
|
|
4837
4853
|
if (submitSelector) {
|
|
4838
|
-
const { createCursor } = require('ghost-cursor');
|
|
4854
|
+
const { createCursor } = require('ghost-cursor-patchright');
|
|
4839
4855
|
const cursor = createCursor(page);
|
|
4840
4856
|
await cursor.click(submitSelector);
|
|
4841
4857
|
|
package/src/mcp/server.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
// CRITICAL: Redirect ALL console.log to STDERR before ANY imports
|
|
9
9
|
// MCP uses STDIO transport — STDOUT must contain ONLY JSON-RPC messages.
|
|
10
|
-
// Any console.log from this code or ANY dependency (
|
|
10
|
+
// Any console.log from this code or ANY dependency (playwright, blocker, etc.)
|
|
11
11
|
// will corrupt the JSON-RPC stream and cause parsing errors.
|
|
12
12
|
const _originalConsoleLog = console.log;
|
|
13
13
|
console.log = function (...args) {
|