mcp-camoufox 0.5.1 → 0.5.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/README.md +35 -6
- package/dist/index.js +93 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
The most feature-rich stealth browser MCP server. **
|
|
14
|
+
The most feature-rich stealth browser MCP server. **80 tools** for full browser control powered by [Camoufox](https://github.com/daijro/camoufox) — a Firefox fork with C++ level anti-detection that bypasses Cloudflare, bot detection, and anti-automation.
|
|
15
15
|
|
|
16
16
|
> **One command. No Python. No manual setup. Everything auto-installs.**
|
|
17
17
|
|
|
@@ -39,7 +39,35 @@ claude mcp add camoufox -- npx -y mcp-camoufox@latest
|
|
|
39
39
|
| redf0x1/camofox-mcp | 45 | Yes | No (clone) | Yes |
|
|
40
40
|
| Sekinal/camoufox-mcp | 49 | Yes | No (clone) | Yes |
|
|
41
41
|
| Playwright CLI | 60+ | No | Yes | Yes |
|
|
42
|
-
| **[mcp-camoufox](https://github.com/RobithYusuf/mcp-camoufox)** | **
|
|
42
|
+
| **[mcp-camoufox](https://github.com/RobithYusuf/mcp-camoufox)** | **80** | **Yes** | **Yes** | **Yes** |
|
|
43
|
+
|
|
44
|
+
## Proven on Real Sites
|
|
45
|
+
|
|
46
|
+
| Site | Challenge | Result |
|
|
47
|
+
|------|-----------|--------|
|
|
48
|
+
| `2captcha.com/demo/cloudflare-turnstile` | Cloudflare Turnstile widget | ✅ **"Success!"** via `click_turnstile()` tool ([proof](docs/images/turnstile.jpg)) |
|
|
49
|
+
| `bot.sannysoft.com` | Firefox fingerprint tests | ✅ All green ([proof](docs/images/sannysoft.jpg)) |
|
|
50
|
+
| `browserscan.net/bot-detection` | WebDriver/UA/CDP/Navigator | ✅ All categories "Normal" ([proof](docs/images/browserscan.jpg)) |
|
|
51
|
+
|
|
52
|
+
### 🎯 Cloudflare Turnstile → Success via `click_turnstile()`
|
|
53
|
+
|
|
54
|
+
<img src="docs/images/turnstile.jpg" alt="Cloudflare Turnstile success" width="500">
|
|
55
|
+
|
|
56
|
+
`click_turnstile()` auto-detects the widget via 6 selector fallback (`iframe[src*=challenges.cloudflare.com]`, `[data-sitekey]`, `.cf-turnstile`, …), computes checkbox position (offset_x=30 from widget left), and clicks with a 3-step Bezier-like approach — combined with Camoufox's native `humanize` + `disable_coop` for cross-origin iframe click.
|
|
57
|
+
|
|
58
|
+
**Scope:** works on **Interactive Turnstile** (visible iframe widget). **Managed Challenge** interstitials ("Just a moment...") render the widget in shadow DOM — not supported here; use sister project [mcp-stealth-chrome](https://github.com/RobithYusuf/mcp-stealth-chrome) (Chrome+CDP) for those. Real-world bypass success also depends on IP reputation and browser fingerprint — code alone doesn't guarantee it.
|
|
59
|
+
|
|
60
|
+
### 🧪 bot.sannysoft.com → Firefox Fingerprint Pass
|
|
61
|
+
|
|
62
|
+
<img src="docs/images/sannysoft.jpg" alt="sannysoft Firefox pass" width="500">
|
|
63
|
+
|
|
64
|
+
User Agent reports `Firefox/135.0`, WebDriver missing, WebDriver Advanced passed, Permissions prompt, Plugins length 5 passed, Languages `en-US,en`, WebGL Intel HD Graphics — all green. ("Chrome: missing" is expected — Camoufox spoofs Firefox, not Chrome.)
|
|
65
|
+
|
|
66
|
+
### 🔍 browserscan.net/bot-detection → All Categories Normal
|
|
67
|
+
|
|
68
|
+
<img src="docs/images/browserscan.jpg" alt="browserscan normal" width="500">
|
|
69
|
+
|
|
70
|
+
WebDriver, User-Agent, CDP, Navigator — every detection category returns **"Normal"**. Camoufox's C++-level Firefox patches leave zero automation signals.
|
|
43
71
|
|
|
44
72
|
## Setup
|
|
45
73
|
|
|
@@ -234,7 +262,7 @@ Or via UI: Agent Panel > `...` > MCP Servers > Manage MCP Servers > View raw con
|
|
|
234
262
|
|
|
235
263
|
That's all. Camoufox browser binary (~80MB) downloads automatically on first launch.
|
|
236
264
|
|
|
237
|
-
## All
|
|
265
|
+
## All 80 Tools
|
|
238
266
|
|
|
239
267
|
### Browser Lifecycle (2)
|
|
240
268
|
|
|
@@ -283,12 +311,13 @@ That's all. Camoufox browser binary (~80MB) downloads automatically on first lau
|
|
|
283
311
|
| `type_text` | Type char by char. Options: `delay`. For OTP, masked inputs, date pickers. |
|
|
284
312
|
| `press_key` | Key or combo: `Enter`, `Escape`, `Tab`, `Control+a`, `Meta+c` |
|
|
285
313
|
|
|
286
|
-
### Mouse XY (
|
|
314
|
+
### Mouse XY (4)
|
|
287
315
|
|
|
288
316
|
| Tool | Description |
|
|
289
317
|
|------|-------------|
|
|
290
|
-
| `mouse_click_xy` | Click at exact coordinates |
|
|
291
|
-
| `mouse_move` | Move cursor to coordinates |
|
|
318
|
+
| `mouse_click_xy` | Click at exact coordinates. Optional `steps` (0=instant, 15-30=human-like pre-movement) |
|
|
319
|
+
| `mouse_move` | Move cursor to coordinates. Optional `steps` for interpolated path |
|
|
320
|
+
| `click_turnstile` | Auto-find + humanized click on Cloudflare Turnstile widget. Params: `offset_x` (default 30), `offset_y`, `wait_render_ms`. Works on Interactive Turnstile (visible iframe widget). Not for Managed Challenge interstitials. |
|
|
292
321
|
| `drag_and_drop` | Drag between two elements |
|
|
293
322
|
|
|
294
323
|
### Wait (4)
|
package/dist/index.js
CHANGED
|
@@ -113,6 +113,37 @@ const server = new McpServer({
|
|
|
113
113
|
version: "0.2.0",
|
|
114
114
|
});
|
|
115
115
|
// ── Tools: Browser Lifecycle ───────────────────────────────────────────────
|
|
116
|
+
async function ensureCamoufoxBinary() {
|
|
117
|
+
const { execSync } = await import("child_process");
|
|
118
|
+
const { existsSync } = await import("fs");
|
|
119
|
+
const { join: pathJoin } = await import("path");
|
|
120
|
+
const os = await import("os");
|
|
121
|
+
const homeDir = os.homedir();
|
|
122
|
+
const platform = os.platform();
|
|
123
|
+
let cacheDir;
|
|
124
|
+
if (platform === "darwin") {
|
|
125
|
+
cacheDir = pathJoin(homeDir, "Library", "Caches", "camoufox");
|
|
126
|
+
}
|
|
127
|
+
else if (platform === "win32") {
|
|
128
|
+
cacheDir = pathJoin(process.env.LOCALAPPDATA || pathJoin(homeDir, "AppData", "Local"), "camoufox");
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
cacheDir = pathJoin(process.env.XDG_CACHE_HOME || pathJoin(homeDir, ".cache"), "camoufox");
|
|
132
|
+
}
|
|
133
|
+
const versionFile = pathJoin(cacheDir, "version.json");
|
|
134
|
+
if (existsSync(versionFile))
|
|
135
|
+
return;
|
|
136
|
+
console.error("\n" + "=".repeat(60));
|
|
137
|
+
console.error("[mcp-camoufox] First-time setup: downloading Camoufox (~500MB)");
|
|
138
|
+
console.error("[mcp-camoufox] Please wait 2-5 minutes...");
|
|
139
|
+
console.error("=".repeat(60) + "\n");
|
|
140
|
+
const cmd = platform === "win32" ? "npx.cmd" : "npx";
|
|
141
|
+
execSync(`${cmd} camoufox-js fetch`, {
|
|
142
|
+
stdio: "inherit", timeout: 900000,
|
|
143
|
+
env: { ...process.env, npm_config_yes: "true" },
|
|
144
|
+
});
|
|
145
|
+
console.error("\n[mcp-camoufox] Download complete.\n");
|
|
146
|
+
}
|
|
116
147
|
server.tool("browser_launch", "Launch Camoufox stealth browser and navigate to URL. Browser persists between calls. Call this first.", {
|
|
117
148
|
url: z.string().default("about:blank").describe("URL to navigate to"),
|
|
118
149
|
headless: z.boolean().default(true).describe("Run without visible window"),
|
|
@@ -133,57 +164,7 @@ server.tool("browser_launch", "Launch Camoufox stealth browser and navigate to U
|
|
|
133
164
|
ensureDirs();
|
|
134
165
|
const w = width > 0 ? width : 1280;
|
|
135
166
|
const h = height > 0 ? height : 800;
|
|
136
|
-
|
|
137
|
-
// camoufox-js does NOT auto-download — we handle it here
|
|
138
|
-
await (async () => {
|
|
139
|
-
const { execSync } = await import("child_process");
|
|
140
|
-
const { existsSync, readdirSync } = await import("fs");
|
|
141
|
-
const { join: pathJoin } = await import("path");
|
|
142
|
-
const os = await import("os");
|
|
143
|
-
// Detect cache dir per platform (same logic as camoufox-js pkgman.ts)
|
|
144
|
-
const homeDir = os.homedir();
|
|
145
|
-
const platform = os.platform();
|
|
146
|
-
let cacheDir;
|
|
147
|
-
if (platform === "darwin") {
|
|
148
|
-
cacheDir = pathJoin(homeDir, "Library", "Caches", "camoufox");
|
|
149
|
-
}
|
|
150
|
-
else if (platform === "win32") {
|
|
151
|
-
cacheDir = pathJoin(process.env.LOCALAPPDATA || pathJoin(homeDir, "AppData", "Local"), "camoufox");
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
cacheDir = pathJoin(process.env.XDG_CACHE_HOME || pathJoin(homeDir, ".cache"), "camoufox");
|
|
155
|
-
}
|
|
156
|
-
// Check if binary exists (look for version.json inside cache dir)
|
|
157
|
-
const versionFile = pathJoin(cacheDir, "version.json");
|
|
158
|
-
const isInstalled = existsSync(versionFile);
|
|
159
|
-
if (!isInstalled) {
|
|
160
|
-
console.error("");
|
|
161
|
-
console.error("=".repeat(60));
|
|
162
|
-
console.error("[mcp-camoufox] First-time setup: downloading Camoufox browser");
|
|
163
|
-
console.error("[mcp-camoufox] This is ~500MB and only happens once.");
|
|
164
|
-
console.error("[mcp-camoufox] Please wait 2-5 minutes...");
|
|
165
|
-
console.error("=".repeat(60));
|
|
166
|
-
console.error("");
|
|
167
|
-
try {
|
|
168
|
-
// Use npx to run camoufox-js CLI fetch command
|
|
169
|
-
const cmd = platform === "win32" ? "npx.cmd" : "npx";
|
|
170
|
-
execSync(`${cmd} camoufox-js fetch`, {
|
|
171
|
-
stdio: "inherit",
|
|
172
|
-
timeout: 900000, // 15 min max
|
|
173
|
-
env: { ...process.env, npm_config_yes: "true" },
|
|
174
|
-
});
|
|
175
|
-
console.error("");
|
|
176
|
-
console.error("[mcp-camoufox] Download complete! Browser ready.");
|
|
177
|
-
console.error("");
|
|
178
|
-
}
|
|
179
|
-
catch (fetchErr) {
|
|
180
|
-
console.error(`[mcp-camoufox] Auto-download failed: ${fetchErr.message?.slice(0, 200)}`);
|
|
181
|
-
console.error("[mcp-camoufox] Try manually: npx camoufox-js fetch");
|
|
182
|
-
throw new Error("Camoufox browser binary not found. Auto-download failed. " +
|
|
183
|
-
"Please run manually: npx camoufox-js fetch");
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
})();
|
|
167
|
+
await ensureCamoufoxBinary();
|
|
187
168
|
const ctx = await Camoufox({
|
|
188
169
|
headless,
|
|
189
170
|
humanize,
|
|
@@ -823,21 +804,74 @@ server.tool("sessionstorage_set", "Set a sessionStorage item.", {
|
|
|
823
804
|
return { content: [{ type: "text", text: `sessionStorage set: ${key}` }] };
|
|
824
805
|
});
|
|
825
806
|
// ── Tools: Mouse XY ────────────────────────────────────────────────────────
|
|
826
|
-
server.tool("mouse_click_xy", "Click at exact x,y coordinates
|
|
807
|
+
server.tool("mouse_click_xy", "Click at exact x,y coordinates. steps>0 adds interpolated pre-movement (human-like).", {
|
|
827
808
|
x: z.number(), y: z.number(),
|
|
828
809
|
button: z.enum(["left", "right", "middle"]).default("left"),
|
|
829
|
-
|
|
810
|
+
steps: z.number().default(0).describe("Interpolation steps for pre-click movement (0=instant, 15-30=human-like)"),
|
|
811
|
+
}, async ({ x, y, button, steps }) => {
|
|
830
812
|
const page = getPage();
|
|
813
|
+
if (steps > 0) {
|
|
814
|
+
await page.mouse.move(x, y, { steps });
|
|
815
|
+
await page.waitForTimeout(80 + Math.random() * 60);
|
|
816
|
+
}
|
|
831
817
|
await page.mouse.click(x, y, { button });
|
|
832
818
|
await page.waitForTimeout(500);
|
|
833
|
-
return { content: [{ type: "text", text: `Clicked at (${x}, ${y}) button=${button}` }] };
|
|
819
|
+
return { content: [{ type: "text", text: `Clicked at (${x}, ${y}) button=${button} steps=${steps}` }] };
|
|
834
820
|
});
|
|
835
|
-
server.tool("mouse_move", "Move mouse to
|
|
821
|
+
server.tool("mouse_move", "Move mouse to x,y. steps>0 interpolates path (human-like).", {
|
|
836
822
|
x: z.number(), y: z.number(),
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
823
|
+
steps: z.number().default(0).describe("Interpolation steps (0=instant jump, 15-30=smooth)"),
|
|
824
|
+
}, async ({ x, y, steps }) => {
|
|
825
|
+
const page = getPage();
|
|
826
|
+
await page.mouse.move(x, y, steps > 0 ? { steps } : undefined);
|
|
827
|
+
return { content: [{ type: "text", text: `Mouse moved to (${x}, ${y}) steps=${steps}` }] };
|
|
828
|
+
});
|
|
829
|
+
server.tool("click_turnstile", "Auto-find and click Cloudflare Turnstile checkbox. Port of mcp-stealth-chrome's proven pattern — single pre-drift + direct click, leaning on Camoufox's built-in humanize + disable_coop for cross-origin iframe support. Works on Interactive Turnstile widgets (visible iframe). Managed Challenge interstitials not supported — use mcp-stealth-chrome for those.", {
|
|
830
|
+
offset_x: z.number().default(30).describe("Pixels from widget left edge (calibrated for CF checkbox)"),
|
|
831
|
+
offset_y: z.number().optional().describe("Vertical offset from widget top (default = height/2)"),
|
|
832
|
+
wait_render_ms: z.number().default(500).describe("Wait before detection to let widget render"),
|
|
833
|
+
}, async ({ offset_x, offset_y, wait_render_ms }) => {
|
|
834
|
+
const page = getPage();
|
|
835
|
+
await page.waitForTimeout(wait_render_ms);
|
|
836
|
+
// Widget detection — 6 selectors ordered by specificity (port from mcp-stealth-chrome)
|
|
837
|
+
const coords = await page.evaluate(() => {
|
|
838
|
+
const sels = [
|
|
839
|
+
'iframe[src*="challenges.cloudflare.com"]',
|
|
840
|
+
'iframe[src*="turnstile"]',
|
|
841
|
+
'[data-testid*="challenge-widget"]',
|
|
842
|
+
'[data-testid*="turnstile"]',
|
|
843
|
+
'[data-sitekey]',
|
|
844
|
+
'.cf-turnstile',
|
|
845
|
+
];
|
|
846
|
+
for (const sel of sels) {
|
|
847
|
+
const el = document.querySelector(sel);
|
|
848
|
+
if (!el)
|
|
849
|
+
continue;
|
|
850
|
+
const r = el.getBoundingClientRect();
|
|
851
|
+
if (r.width < 50 || r.height < 20)
|
|
852
|
+
continue;
|
|
853
|
+
return {
|
|
854
|
+
found: sel,
|
|
855
|
+
left: Math.round(r.left),
|
|
856
|
+
top: Math.round(r.top),
|
|
857
|
+
width: Math.round(r.width),
|
|
858
|
+
height: Math.round(r.height),
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
return null;
|
|
862
|
+
});
|
|
863
|
+
if (!coords) {
|
|
864
|
+
return { content: [{ type: "text", text: "Turnstile widget not found — selector miss. Likely a Managed Challenge interstitial (use mcp-stealth-chrome) or widget hasn't rendered yet (try wait_render_ms=3000)." }] };
|
|
865
|
+
}
|
|
866
|
+
const targetX = coords.left + offset_x;
|
|
867
|
+
const targetY = coords.top + (offset_y ?? Math.floor(coords.height / 2));
|
|
868
|
+
// Single pre-drift then direct click (matches stealth-chrome's pattern).
|
|
869
|
+
// Camoufox's humanize layer handles path curvature + timing automatically,
|
|
870
|
+
// so extra Bezier hops would be redundant and slow (~3s extra).
|
|
871
|
+
await page.mouse.move(targetX + 180, targetY - 80, { steps: 15 });
|
|
872
|
+
await page.waitForTimeout(150);
|
|
873
|
+
await page.mouse.click(targetX, targetY);
|
|
874
|
+
return { content: [{ type: "text", text: `clicked Turnstile at (${targetX},${targetY}) — widget found via ${coords.found} (${coords.width}x${coords.height})` }] };
|
|
841
875
|
});
|
|
842
876
|
server.tool("drag_and_drop", "Drag from one element to another.", {
|
|
843
877
|
source_ref: z.string().describe("Ref of element to drag"),
|
package/package.json
CHANGED