mcp-camoufox 0.4.9 → 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.
- package/dist/index.js +178 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1279,6 +1279,184 @@ server.tool("scrape_page", "Smart page scraper — auto-detect and extract main
|
|
|
1279
1279
|
})()`);
|
|
1280
1280
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1281
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
|
+
});
|
|
1282
1460
|
// ── Start Server ───────────────────────────────────────────────────────────
|
|
1283
1461
|
async function main() {
|
|
1284
1462
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED