mcp-camoufox 0.4.8 → 0.5.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 (2) hide show
  1. package/dist/index.js +223 -16
  2. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -134,27 +134,56 @@ server.tool("browser_launch", "Launch Camoufox stealth browser and navigate to U
134
134
  const w = width > 0 ? width : 1280;
135
135
  const h = height > 0 ? height : 800;
136
136
  // Auto-download Camoufox binary if not installed
137
- try {
137
+ // camoufox-js does NOT auto-download — we handle it here
138
+ await (async () => {
138
139
  const { execSync } = await import("child_process");
139
140
  const { existsSync, readdirSync } = await import("fs");
140
141
  const { join: pathJoin } = await import("path");
141
- // Check common cache locations
142
- const homeDir = process.env.HOME || process.env.USERPROFILE || "";
143
- const cacheLocations = [
144
- pathJoin(homeDir, ".cache", "camoufox"),
145
- pathJoin(homeDir, "Library", "Caches", "camoufox"),
146
- pathJoin(homeDir, "AppData", "Local", "camoufox"),
147
- ];
148
- const isInstalled = cacheLocations.some(dir => existsSync(dir) && readdirSync(dir).length > 2);
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);
149
159
  if (!isInstalled) {
150
- console.error("[mcp-camoufox] Camoufox browser not found. Auto-downloading (~500MB, one-time)...");
151
- execSync("npx camoufox-js fetch", { stdio: "inherit", timeout: 600000 });
152
- console.error("[mcp-camoufox] Download complete.");
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
+ }
153
185
  }
154
- }
155
- catch (e) {
156
- console.error(`[mcp-camoufox] Auto-download check: ${e.message?.slice(0, 100)}`);
157
- }
186
+ })();
158
187
  const ctx = await Camoufox({
159
188
  headless,
160
189
  humanize,
@@ -1250,6 +1279,184 @@ server.tool("scrape_page", "Smart page scraper — auto-detect and extract main
1250
1279
  })()`);
1251
1280
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1252
1281
  });
1282
+ // ── Tools: Compound (reduce round-trips) ───────────────────────────────────
1283
+ server.tool("wait_and_snapshot", "Wait for selector/text then return snapshot. Combines wait_for + browser_snapshot in one call.", {
1284
+ selector: z.string().default("").describe("CSS selector to wait for"),
1285
+ text: z.string().default("").describe("Text to wait for"),
1286
+ state: z.enum(["visible", "hidden", "attached", "detached"]).default("visible"),
1287
+ timeout: z.number().default(10000),
1288
+ }, async ({ selector, text, state, timeout }) => {
1289
+ const page = getPage();
1290
+ if (selector) {
1291
+ await page.locator(selector).first().waitFor({ state, timeout });
1292
+ }
1293
+ else if (text) {
1294
+ await page.getByText(text).first().waitFor({ state, timeout });
1295
+ }
1296
+ const elements = await page.evaluate(SNAPSHOT_JS) || [];
1297
+ const snap = formatSnapshot(elements, page.url(), await page.title());
1298
+ return { content: [{ type: "text", text: snap }] };
1299
+ });
1300
+ server.tool("back_and_snapshot", "Navigate back + return snapshot.", {}, async () => {
1301
+ const page = getPage();
1302
+ await page.goBack({ waitUntil: "domcontentloaded", timeout: 15000 });
1303
+ await page.waitForTimeout(500);
1304
+ const elements = await page.evaluate(SNAPSHOT_JS) || [];
1305
+ const snap = formatSnapshot(elements, page.url(), await page.title());
1306
+ return { content: [{ type: "text", text: snap }] };
1307
+ });
1308
+ server.tool("reload_and_snapshot", "Reload page + return snapshot.", {}, async () => {
1309
+ const page = getPage();
1310
+ await page.reload({ waitUntil: "domcontentloaded", timeout: 15000 });
1311
+ await page.waitForTimeout(500);
1312
+ const elements = await page.evaluate(SNAPSHOT_JS) || [];
1313
+ const snap = formatSnapshot(elements, page.url(), await page.title());
1314
+ return { content: [{ type: "text", text: snap }] };
1315
+ });
1316
+ server.tool("click_and_snapshot", "Click element by ref + wait + return snapshot. Perfect for buttons that trigger navigation/dialog.", {
1317
+ ref: z.string().describe("Element ref from browser_snapshot"),
1318
+ wait_ms: z.number().default(1500).describe("Wait after click before snapshot"),
1319
+ }, async ({ ref, wait_ms }) => {
1320
+ const page = getPage();
1321
+ const loc = page.locator(`[data-mcp-ref="${ref}"]`).first();
1322
+ try {
1323
+ await loc.click({ timeout: 5000 });
1324
+ }
1325
+ catch {
1326
+ await loc.evaluate((el) => el.click());
1327
+ }
1328
+ await page.waitForTimeout(wait_ms);
1329
+ const elements = await page.evaluate(SNAPSHOT_JS) || [];
1330
+ const snap = formatSnapshot(elements, page.url(), await page.title());
1331
+ return { content: [{ type: "text", text: snap }] };
1332
+ });
1333
+ // ── Tools: Smart Selectors (no snapshot needed) ────────────────────────────
1334
+ server.tool("find_by_text", "Find element by visible text — returns ref ID or null. Skip browser_snapshot if you know exact text.", {
1335
+ text: z.string().describe("Visible text to search for"),
1336
+ exact: z.boolean().default(true),
1337
+ }, async ({ text, exact }) => {
1338
+ const page = getPage();
1339
+ const loc = page.getByText(text, { exact }).first();
1340
+ const count = await loc.count();
1341
+ if (count === 0) {
1342
+ return { content: [{ type: "text", text: `No element found with text "${text}"` }] };
1343
+ }
1344
+ // Tag with ref
1345
+ const info = await loc.evaluate((el) => {
1346
+ const ref = 'f' + Math.floor(Math.random() * 10000);
1347
+ el.setAttribute('data-mcp-ref', ref);
1348
+ return {
1349
+ ref,
1350
+ tag: el.tagName.toLowerCase(),
1351
+ role: el.getAttribute('role') || '',
1352
+ text: (el.innerText || el.value || '').trim().slice(0, 100),
1353
+ href: el.href || '',
1354
+ };
1355
+ });
1356
+ return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
1357
+ });
1358
+ server.tool("find_by_label", "Find input element by its label text (<label>). Returns ref.", {
1359
+ label: z.string().describe("Label text (e.g. 'Email', 'Password')"),
1360
+ }, async ({ label }) => {
1361
+ const page = getPage();
1362
+ const loc = page.getByLabel(label).first();
1363
+ const count = await loc.count();
1364
+ if (count === 0) {
1365
+ return { content: [{ type: "text", text: `No input found with label "${label}"` }] };
1366
+ }
1367
+ const info = await loc.evaluate((el) => {
1368
+ const ref = 'l' + Math.floor(Math.random() * 10000);
1369
+ el.setAttribute('data-mcp-ref', ref);
1370
+ return {
1371
+ ref,
1372
+ tag: el.tagName.toLowerCase(),
1373
+ type: el.type || '',
1374
+ name: el.name || '',
1375
+ placeholder: el.placeholder || '',
1376
+ value: el.value || '',
1377
+ };
1378
+ });
1379
+ return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
1380
+ });
1381
+ server.tool("find_by_placeholder", "Find input by placeholder text. Returns ref.", {
1382
+ placeholder: z.string(),
1383
+ }, async ({ placeholder }) => {
1384
+ const page = getPage();
1385
+ const loc = page.getByPlaceholder(placeholder).first();
1386
+ const count = await loc.count();
1387
+ if (count === 0) {
1388
+ return { content: [{ type: "text", text: `No input with placeholder "${placeholder}"` }] };
1389
+ }
1390
+ const info = await loc.evaluate((el) => {
1391
+ const ref = 'p' + Math.floor(Math.random() * 10000);
1392
+ el.setAttribute('data-mcp-ref', ref);
1393
+ return {
1394
+ ref, tag: el.tagName.toLowerCase(), type: el.type || '', placeholder: el.placeholder || '',
1395
+ };
1396
+ });
1397
+ return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
1398
+ });
1399
+ // ── Tools: Cookie Portability ──────────────────────────────────────────────
1400
+ server.tool("cookie_export", "Export all cookies as JSON string. Use with cookie_import to transfer session.", {
1401
+ domain: z.string().default("").describe("Filter by domain (empty = all)"),
1402
+ }, async ({ domain }) => {
1403
+ if (!browserContext)
1404
+ throw new Error("Browser not running.");
1405
+ let cookies = await browserContext.cookies();
1406
+ if (domain)
1407
+ cookies = cookies.filter(c => c.domain.includes(domain));
1408
+ return { content: [{ type: "text", text: JSON.stringify(cookies, null, 2) }] };
1409
+ });
1410
+ server.tool("cookie_import", "Import cookies from JSON (from cookie_export). Restores session state.", {
1411
+ cookies_json: z.string().describe("JSON array of cookies"),
1412
+ }, async ({ cookies_json }) => {
1413
+ if (!browserContext)
1414
+ throw new Error("Browser not running.");
1415
+ let cookies;
1416
+ try {
1417
+ cookies = JSON.parse(cookies_json);
1418
+ if (!Array.isArray(cookies))
1419
+ throw new Error("not an array");
1420
+ }
1421
+ catch (e) {
1422
+ return { content: [{ type: "text", text: `Invalid cookies JSON: ${e.message}` }] };
1423
+ }
1424
+ await browserContext.addCookies(cookies);
1425
+ return { content: [{ type: "text", text: `Imported ${cookies.length} cookies.` }] };
1426
+ });
1427
+ // ── Tools: Page Stats (debug/decision) ─────────────────────────────────────
1428
+ server.tool("page_stats", "Page statistics: element count, size, load metrics. Use to decide extraction strategy.", {}, async () => {
1429
+ const page = getPage();
1430
+ const stats = await page.evaluate(`(() => {
1431
+ var all = document.querySelectorAll('*').length;
1432
+ var interactive = document.querySelectorAll('button, a, input, select, textarea, [role="button"], [role="link"]').length;
1433
+ var images = document.querySelectorAll('img').length;
1434
+ var forms = document.querySelectorAll('form').length;
1435
+ var iframes = document.querySelectorAll('iframe').length;
1436
+ var scripts = document.querySelectorAll('script').length;
1437
+ var bodyTextLen = (document.body.innerText || '').length;
1438
+ var htmlLen = document.documentElement.outerHTML.length;
1439
+ var perf = window.performance && window.performance.timing ? {
1440
+ domComplete: window.performance.timing.domComplete - window.performance.timing.navigationStart,
1441
+ loadEnd: window.performance.timing.loadEventEnd - window.performance.timing.navigationStart,
1442
+ } : null;
1443
+ return {
1444
+ url: location.href,
1445
+ title: document.title,
1446
+ total_elements: all,
1447
+ interactive_elements: interactive,
1448
+ images: images,
1449
+ forms: forms,
1450
+ iframes: iframes,
1451
+ scripts: scripts,
1452
+ body_text_length: bodyTextLen,
1453
+ html_size: htmlLen,
1454
+ performance_ms: perf,
1455
+ recommendation: all > 3000 ? 'Use extract_structured or scrape_page (heavy page)' : 'browser_snapshot OK',
1456
+ };
1457
+ })()`);
1458
+ return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
1459
+ });
1253
1460
  // ── Start Server ───────────────────────────────────────────────────────────
1254
1461
  async function main() {
1255
1462
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-camoufox",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for stealth browser automation via Camoufox — 39 tools, Chrome DevTools MCP-level power with anti-bot stealth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,8 +16,7 @@
16
16
  "build": "tsc",
17
17
  "start": "node dist/index.js",
18
18
  "dev": "tsc --watch",
19
- "prepublishOnly": "npm run build",
20
- "postinstall": "npx camoufox-js fetch || echo '[mcp-camoufox] Browser will download on first launch.'"
19
+ "prepublishOnly": "npm run build"
21
20
  },
22
21
  "keywords": [
23
22
  "mcp",