UNPKG

camoufox-mcp-server

Version:

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

233 lines (232 loc) 12.4 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { Camoufox } from "camoufox-js"; import chalk from "chalk"; /** * Initialize the MCP (Model Context Protocol) server with Camoufox browsing capabilities * Provides privacy-focused web browsing with anti-detection features */ const server = new McpServer({ name: "camoufox-mcp-server", version: "1.4.0", }); /** * Define the browse tool with comprehensive privacy and anti-detection parameters * This tool allows AI assistants to navigate websites while maintaining user privacy * and avoiding bot detection through various stealth techniques. */ server.tool("browse", { url: z.string().describe("The URL to navigate to and retrieve content from. Use this tool when users ask to visit, check, search, navigate, browse, fetch, or scrape websites. Must be a fully qualified URL (e.g., 'https://example.com')."), os: z.enum(["windows", "macos", "linux"]).optional().describe("Optional OS to spoof. Can be 'windows', 'macos', or 'linux'. If not specified, will rotate between all OS types."), waitStrategy: z.enum(["domcontentloaded", "load", "networkidle"]).optional().default("domcontentloaded").describe("Wait strategy for page load. 'domcontentloaded' waits for DOM, 'load' waits for all resources, 'networkidle' waits for network activity to finish."), timeout: z.number().min(5000).max(300000).optional().default(60000).describe("Timeout in milliseconds for page load (5-300 seconds)."), humanize: z.boolean().optional().default(true).describe("Enable realistic cursor movements and human-like behavior for better stealth and anti-detection. Helps avoid bot detection by simulating natural user interactions."), locale: z.string().optional().describe("Browser locale (e.g., 'en-US', 'fr-FR')."), viewport: z.object({ width: z.number().min(320).max(3840).default(1920), height: z.number().min(240).max(2160).default(1080) }).optional().describe("Custom viewport dimensions."), screenshot: z.boolean().optional().default(false).describe("Capture a screenshot/image of the page after loading. Use when users ask to take a screenshot, capture an image, show them visually, or want to see how the page looks."), block_webrtc: z.boolean().optional().default(true).describe("Block WebRTC entirely for enhanced privacy and stealth. Use when users want private browsing, to hide their real IP, prevent WebRTC leaks, or browse in stealth mode."), proxy: z.union([ z.string().describe("Proxy URL (e.g., 'http://proxy.example.com:8080')"), z.object({ server: z.string().describe("Proxy server URL"), username: z.string().optional().describe("Proxy username for authentication"), password: z.string().optional().describe("Proxy password for authentication") }) ]).optional().describe("Proxy configuration for anonymous browsing. Use when users want to browse through a proxy, hide their IP, browse anonymously, or access content via a specific server location."), enable_cache: z.boolean().optional().default(false).describe("Cache pages, requests, etc. Uses more memory but improves performance when revisiting pages."), firefox_user_prefs: z.record(z.any()).optional().describe("Custom Firefox user preferences to set."), exclude_addons: z.array(z.string()).optional().describe("List of default addons to exclude (e.g., ['ublock_origin'])."), window: z.preprocess((arg) => { if (Array.isArray(arg) && arg.length === 0) { return undefined; } return arg; }, z.tuple([ z.number().min(320).max(3840), z.number().min(240).max(2160) ]).optional()).describe("Set fixed window size [width, height] instead of random generation. An empty array [] is accepted and treated as if the window parameter was not specified."), args: z.array(z.string()).optional().describe("Additional command-line arguments to pass to the browser."), block_images: z.boolean().optional().default(false).describe("Block all images for faster loading, reduced bandwidth, and lightweight browsing. Use when users want quick/fast browsing, text-only content, or to save bandwidth."), block_webgl: z.boolean().optional().default(false).describe("Block WebGL to prevent fingerprinting and tracking. Use for maximum privacy/stealth mode, but note it may cause detection on some sites that rely heavily on WebGL."), disable_coop: z.boolean().optional().default(false).describe("Disable Cross-Origin-Opener-Policy to allow interaction with iframes and cross-origin content. Use when users need to click elements in iframes or access embedded content."), geoip: z.boolean().optional().default(true).describe("Automatically detect geolocation based on IP address."), headless: z.boolean().optional().describe("Run browser in headless mode. Auto-detects best mode for environment if not specified."), }, /** * Browse tool handler function * @param params - Object containing all browsing parameters * @param params.url - Target URL to navigate to * @param params.os - Operating system to spoof * @param params.waitStrategy - Page load wait strategy * @param params.timeout - Page load timeout in milliseconds * @param params.humanize - Enable human-like behavior * @param params.locale - Browser locale setting * @param params.viewport - Custom viewport dimensions * @param params.screenshot - Capture screenshot after loading * @param params.block_webrtc - Block WebRTC for privacy * @param params.proxy - Proxy configuration * @param params.enable_cache - Enable browser caching * @param params.firefox_user_prefs - Custom Firefox preferences * @param params.exclude_addons - Addons to exclude * @param params.window - Fixed window size * @param params.args - Additional browser arguments * @param params.block_images - Block image loading * @param params.block_webgl - Block WebGL * @param params.disable_coop - Disable Cross-Origin-Opener-Policy * @param params.geoip - Auto-detect geolocation * @param params.headless - Headless mode setting * @returns Promise resolving to content with HTML and optional screenshot */ async ({ url, os, waitStrategy, timeout, humanize, locale, viewport, screenshot, block_webrtc, proxy, enable_cache, firefox_user_prefs, exclude_addons, window, args, block_images, block_webgl, disable_coop, geoip, headless }) => { let browser; try { console.error(chalk.blue(`[Camoufox] Launching browser to browse: ${url}`)); // Detect if we're running in Docker/Linux or locally for optimal headless mode const isLinux = process.platform === 'linux'; const headlessMode = headless !== undefined ? headless : (isLinux ? 'virtual' : true); // Auto-rotate OS fingerprint if not specified for better anti-detection const osOptions = ["windows", "macos", "linux"]; const selectedOS = os || osOptions[Math.floor(Math.random() * osOptions.length)]; // Launch Camoufox browser with comprehensive anti-detection and privacy settings browser = await Camoufox({ os: [selectedOS], // Pass as array for BrowserForge rotation headless: headlessMode, humanize: humanize, // Enable realistic cursor movements geoip: geoip, // Auto-detect location based on IP ublock: true, // Keep uBlock Origin for better stealth block_webgl: block_webgl, // Block WebGL if requested block_images: block_images, // Block images if requested block_webrtc: block_webrtc, disable_coop: disable_coop, locale: locale, viewport: viewport ? { width: viewport.width, height: viewport.height } : undefined, proxy: proxy, enable_cache: enable_cache, firefox_user_prefs: firefox_user_prefs, exclude_addons: exclude_addons, window: window, args: args, }); // Type assertion to handle incomplete type definitions const page = await browser.newPage(); await page.goto(url, { waitUntil: waitStrategy, timeout: timeout }); const pageContent = await page.content(); // Get the full HTML content // Capture screenshot if requested, with error handling let screenshotBase64; if (screenshot) { try { const screenshotBuffer = await page.screenshot({ type: 'png' }); screenshotBase64 = screenshotBuffer.toString('base64'); console.error(chalk.green(`[Camoufox] Screenshot captured for ${url}.`)); } catch (screenshotError) { console.error(chalk.yellow(`[Camoufox] Screenshot failed: ${screenshotError instanceof Error ? screenshotError.message : String(screenshotError)}`)); } } // Build feature summary for logging const features = [ `OS: ${selectedOS}`, `wait: ${waitStrategy}`, proxy ? 'proxy: enabled' : null, block_webrtc ? 'WebRTC: blocked' : null, block_images ? 'images: blocked' : null, block_webgl ? 'WebGL: blocked' : null, disable_coop ? 'COOP: disabled' : null, !geoip ? 'geoip: disabled' : null, ].filter(Boolean).join(', '); console.error(chalk.green(`[Camoufox] Successfully retrieved content from ${url} (${features}).`)); const content = [{ type: "text", text: pageContent // Return the HTML content }]; if (screenshotBase64) { content.push({ type: "image", data: screenshotBase64, mimeType: "image/png" }); } return { content }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(chalk.red(`[Camoufox] Error during browsing: ${errorMessage}`)); return { content: [{ type: "text", text: `Failed to browse URL ${url}. Error: ${errorMessage}` }], isError: true }; } finally { if (browser) { console.error(chalk.blue('[Camoufox] Closing browser.')); await browser.close(); } } }); /** * Main function to start the MCP server with stdio transport * Handles server initialization and connection setup * @throws {Error} When server initialization fails */ async function runServer() { try { const transport = new StdioServerTransport(); await server.connect(transport); console.error(chalk.yellow("Camoufox MCP Server is running on stdio...")); } catch (error) { console.error(chalk.red("Fatal error during server initialization:", error)); process.exit(1); } } /** * Handle process termination gracefully on SIGINT (Ctrl+C) * Ensures clean shutdown when user interrupts the process */ process.on('SIGINT', () => { console.error(chalk.yellow('\n[Camoufox] Shutting down server...')); process.exit(0); }); /** * Handle process termination gracefully on SIGTERM * Ensures clean shutdown when process is terminated by system */ process.on('SIGTERM', () => { console.error(chalk.yellow('\n[Camoufox] Shutting down server...')); process.exit(0); }); /** * Handle uncaught exceptions to prevent silent failures * Logs error details and exits with error code * @param error - The uncaught exception */ process.on('uncaughtException', (error) => { console.error(chalk.red('[Camoufox] Uncaught exception:', error)); process.exit(1); }); /** * Handle unhandled promise rejections * Prevents silent failures from unhandled async operations * @param reason - Rejection reason * @param promise - The rejected promise */ process.on('unhandledRejection', (reason, promise) => { console.error(chalk.red('[Camoufox] Unhandled rejection at:', promise, 'reason:', reason)); process.exit(1); }); runServer().catch((error) => { console.error(chalk.red("Fatal error running server:", error)); process.exit(1); });