UNPKG

camoufox-mcp-server

Version:

MCP server for browser automation using Camoufox - a privacy-focused Firefox fork with advanced anti-detection features

184 lines (183 loc) 10.5 kB
import { launchPath } from "camoufox-js/dist/pkgman.js"; import chalk from "chalk"; import { ALLOW_EVALUATE, ALLOW_UNSAFE_OPTIONS, CAPTCHA_AUTONOMOUS, DEFAULT_MAX_CHARS, DEFAULT_MAX_ELEMENTS, MAX_CONCURRENCY, MAX_QUEUE, MAX_SCREENSHOT_HEIGHT, MAX_SCREENSHOT_WIDTH, MAX_SESSIONS, SEQUENCE_TIMEOUT_MS, SERVER_VERSION, SESSION_TTL_MS, buildNetworkSecurityStatus } from "./config.js"; import { activeBrowserCount, queuedBrowserRequestCount, runBrowserOperation, runGuardedPageRead } from "./browser-runtime.js"; import { maybeDetectCaptcha } from "./captcha.js"; import { activeSessionCount } from "./sessions.js"; import { buildBrowsePayload, buildSnapshotPayload } from "./extractors.js"; import { buildSuccessContent, buildToolError, buildToolFailure } from "./responses.js"; import { captureScreenshot, isScreenshotDimensionAllowed } from "./screenshots.js"; import { runSequenceActionsWithBudget, sequenceTimeoutBudget } from "./sequence.js"; import { applyStealthProfile, defaultHeadlessMode, getProxySecrets, getProxyServer, redactUrl } from "./utils.js"; import { appendDiagnostics } from "./diagnostics.js"; export function buildFeatureSummary(selectedOS, waitStrategy, outputMode, charLimit, payload, proxy, blockWebrtc, blockImages, blockWebgl, disableCoop, geoip) { const features = [ `OS: ${selectedOS}`, `wait: ${waitStrategy}`, `output: ${outputMode}`, payload.truncated ? `truncated: ${charLimit}` : undefined, proxy ? "proxy: enabled" : undefined, blockWebrtc ? "WebRTC: blocked" : undefined, blockImages ? "images: blocked" : undefined, blockWebgl ? "WebGL: blocked" : undefined, disableCoop ? "COOP: disabled" : undefined, !geoip ? "geoip: disabled" : undefined, ].filter((feature) => feature !== undefined); return features.join(", "); } export function isBlockedNavigationResponse(payload) { if (payload.status !== 403) { return false; } const content = (payload.text ?? payload.html ?? "").toLowerCase(); return content.includes("forbidden redirect url") || content.includes("blocked redirect"); } export function buildStatusPayload() { let browserAvailable; let browserPath; try { browserPath = String(launchPath()); browserAvailable = true; } catch { browserAvailable = false; } return { version: SERVER_VERSION, browser: "camoufox", browserAvailable, browserPath, headlessMode: defaultHeadlessMode(undefined), platform: process.platform, activeBrowsers: activeBrowserCount(), activeSessions: activeSessionCount(), queuedRequests: queuedBrowserRequestCount(), maxConcurrency: MAX_CONCURRENCY, maxQueue: MAX_QUEUE, maxSessions: MAX_SESSIONS, sessionTtlMs: SESSION_TTL_MS, unsafeOptionsAllowed: ALLOW_UNSAFE_OPTIONS, evaluateAllowed: ALLOW_EVALUATE, captchaAutonomous: CAPTCHA_AUTONOMOUS, networkSecurity: buildNetworkSecurityStatus(), }; } export async function handleStatus() { return buildSuccessContent(buildStatusPayload()); } export async function handleBrowse(input) { const effectiveInput = applyStealthProfile(input); const safeUrl = redactUrl(effectiveInput.url); if (effectiveInput.screenshot && !isScreenshotDimensionAllowed(effectiveInput.viewport, effectiveInput.window)) { return buildToolError(`Screenshot dimensions exceed server policy (${MAX_SCREENSHOT_WIDTH}x${MAX_SCREENSHOT_HEIGHT}).`); } try { return await runBrowserOperation("browse", effectiveInput, async ({ page, response, requestGuard, diagnostics, selectedOS, waitStrategy, }) => { const mode = effectiveInput.outputMode ?? "text"; const charLimit = effectiveInput.maxChars ?? DEFAULT_MAX_CHARS; const payload = await runGuardedPageRead(page, requestGuard, () => buildBrowsePayload(page, response, mode, charLimit, effectiveInput.selector)); requestGuard.assertAllowed(); if (isBlockedNavigationResponse(payload)) { return buildToolError(`Blocked unsafe browser request to ${safeUrl}.`); } appendDiagnostics(payload, diagnostics.payload()); let screenshotResult; if (effectiveInput.screenshot) { screenshotResult = await captureScreenshot(page, safeUrl, effectiveInput.screenshotOptions); payload.screenshot = screenshotResult.screenshotMetadata; } requestGuard.assertAllowed(); const features = buildFeatureSummary(selectedOS, waitStrategy, mode, charLimit, payload, effectiveInput.proxy, effectiveInput.block_webrtc, effectiveInput.block_images, effectiveInput.block_webgl, effectiveInput.disable_coop, effectiveInput.geoip); console.error(chalk.green(`[Camoufox] Successfully retrieved content from ${safeUrl} (${features}).`)); if (effectiveInput.captchaPolicy) { const { mergedPayload, captchaScreenshot } = await maybeDetectCaptcha(page, response, payload, effectiveInput.captchaPolicy, safeUrl); return buildSuccessContent(mergedPayload, screenshotResult ?? captchaScreenshot); } return buildSuccessContent(payload, screenshotResult); }); } catch (error) { return buildToolFailure("browse", safeUrl, error, effectiveInput); } } export async function handleSnapshot(input) { const effectiveInput = applyStealthProfile(input); const safeUrl = redactUrl(effectiveInput.url); try { return await runBrowserOperation("browse snapshot", effectiveInput, async ({ page, response, requestGuard, diagnostics, }) => { const payload = await runGuardedPageRead(page, requestGuard, () => buildSnapshotPayload(page, response, effectiveInput.maxChars ?? DEFAULT_MAX_CHARS, effectiveInput.maxElements ?? DEFAULT_MAX_ELEMENTS, effectiveInput.selector)); requestGuard.assertAllowed(); appendDiagnostics(payload, diagnostics.payload()); console.error(chalk.green(`[Camoufox] Successfully captured snapshot from ${safeUrl}.`)); if (effectiveInput.captchaPolicy) { const { mergedPayload, captchaScreenshot } = await maybeDetectCaptcha(page, response, payload, effectiveInput.captchaPolicy, safeUrl); return buildSuccessContent(mergedPayload, captchaScreenshot); } return buildSuccessContent(payload); }); } catch (error) { return buildToolFailure("browse snapshot", safeUrl, error, effectiveInput); } } export async function handleSequence(input) { const effectiveInput = applyStealthProfile(input); const safeUrl = redactUrl(effectiveInput.url); if (effectiveInput.screenshot && !isScreenshotDimensionAllowed(effectiveInput.viewport, effectiveInput.window)) { return buildToolError(`Screenshot dimensions exceed server policy (${MAX_SCREENSHOT_WIDTH}x${MAX_SCREENSHOT_HEIGHT}).`); } if (sequenceTimeoutBudget(effectiveInput.actions) > SEQUENCE_TIMEOUT_MS) { return buildToolError(`Sequence timeout budget exceeds server policy (${SEQUENCE_TIMEOUT_MS}ms).`); } try { return await runBrowserOperation("browse sequence", effectiveInput, async ({ page, response, requestGuard, diagnostics, getLastNavigationResponse, }) => { const rawUrls = [effectiveInput.url, getProxyServer(effectiveInput.proxy)].filter((rawUrl) => Boolean(rawUrl)); const secrets = getProxySecrets(effectiveInput.proxy); const actions = await runSequenceActionsWithBudget(page, requestGuard, effectiveInput.actions, rawUrls, secrets); const mode = effectiveInput.outputMode ?? "text"; const charLimit = effectiveInput.maxChars ?? DEFAULT_MAX_CHARS; const finalResponse = getLastNavigationResponse() ?? response; const contentPayload = await runGuardedPageRead(page, requestGuard, () => buildBrowsePayload(page, finalResponse, mode, charLimit, effectiveInput.selector)); requestGuard.assertAllowed(); if (isBlockedNavigationResponse(contentPayload)) { return buildToolError(`Blocked unsafe browser request to ${safeUrl}.`); } const snapshot = await runGuardedPageRead(page, requestGuard, () => buildSnapshotPayload(page, finalResponse, charLimit, effectiveInput.maxElements ?? DEFAULT_MAX_ELEMENTS, effectiveInput.selector)); requestGuard.assertAllowed(); const payload = { url: contentPayload.url, title: contentPayload.title, status: contentPayload.status, contentType: contentPayload.contentType, initialStatus: response?.status(), actions, snapshot, outputMode: mode, truncated: contentPayload.truncated, maxChars: charLimit, selector: effectiveInput.selector, selectorFound: contentPayload.selectorFound, text: contentPayload.text, html: contentPayload.html, }; appendDiagnostics(payload, diagnostics.payload()); let screenshotResult; if (effectiveInput.screenshot) { screenshotResult = await captureScreenshot(page, safeUrl, effectiveInput.screenshotOptions); payload.screenshot = screenshotResult.screenshotMetadata; } requestGuard.assertAllowed(); console.error(chalk.green(`[Camoufox] Successfully ran ${actions.length} actions from ${safeUrl}.`)); if (effectiveInput.captchaPolicy) { const finalResponse = getLastNavigationResponse() ?? response; const { mergedPayload, captchaScreenshot } = await maybeDetectCaptcha(page, finalResponse, payload, effectiveInput.captchaPolicy, safeUrl); return buildSuccessContent(mergedPayload, screenshotResult ?? captchaScreenshot); } return buildSuccessContent(payload, screenshotResult); }); } catch (error) { return buildToolFailure("browse sequence", safeUrl, error, effectiveInput); } } export { handleConsole, handleFind, handleForms, handleLinks, handleNetworkSummary, handleOutline, handleScreenshot, } from "./focused-tool-handlers.js";