UNPKG

camoufox-mcp-server

Version:

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

89 lines (88 loc) 6.95 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import chalk from "chalk"; import { SERVER_VERSION, assertNetworkSandboxPolicy } from "./config.js"; import { anyOutputSchema, browseToolShape, consoleToolShape, findOutputSchema, findToolShape, formsOutputSchema, formsToolShape, linksOutputSchema, linksToolShape, networkSummaryOutputSchema, networkSummaryToolShape, outlineOutputSchema, outlineToolShape, screenshotToolShape, sequenceToolShape, sessionActionToolShape, sessionCloseToolShape, sessionNavigateToolShape, sessionResumeToolShape, sessionSnapshotToolShape, sessionStartToolShape, snapshotToolShape, statusOutputSchema } from "./schemas.js"; import { handleBrowse, handleConsole, handleFind, handleForms, handleLinks, handleNetworkSummary, handleOutline, handleScreenshot, handleSequence, handleSnapshot, handleStatus } from "./tool-handlers.js"; import { closeActiveSessions, handleSessionAction, handleSessionClose, handleSessionNavigate, handleSessionResume, handleSessionSnapshot, handleSessionStart } from "./sessions.js"; import { closeActiveBrowsers, rejectPendingBrowses, setBrowserShuttingDown } from "./browser-runtime.js"; import { describeError } from "./utils.js"; const server = new McpServer({ name: "camoufox-mcp-server", version: SERVER_VERSION }); const readOnlyOpenWorld = { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }; const nonReadOnlyOpenWorld = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }; function registerJsonTool(name, description, inputSchema, annotations, handler, outputSchema = anyOutputSchema) { const registerTool = server.registerTool.bind(server); registerTool(name, { description, inputSchema, outputSchema, annotations }, async (input) => handler(input)); } server.registerTool("camoufox_status", { description: "Return server, browser, queue, session, and policy status without launching a page.", inputSchema: {}, outputSchema: statusOutputSchema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async () => handleStatus()); registerJsonTool("browse", "Navigate once and return bounded page content.", browseToolShape, readOnlyOpenWorld, async (input) => handleBrowse(input)); registerJsonTool("browse_snapshot", "Navigate once and return visible text, ARIA snapshot, and interactive metadata.", snapshotToolShape, readOnlyOpenWorld, async (input) => handleSnapshot(input)); registerJsonTool("browse_sequence", "Navigate once, run bounded selector actions, then return final state.", sequenceToolShape, nonReadOnlyOpenWorld, async (input) => handleSequence(input)); registerJsonTool("browse_links", "Navigate once and return only visible navigable links.", linksToolShape, readOnlyOpenWorld, async (input) => handleLinks(input), linksOutputSchema); registerJsonTool("browse_forms", "Navigate once and return form fields and submit controls.", formsToolShape, readOnlyOpenWorld, async (input) => handleForms(input), formsOutputSchema); registerJsonTool("browse_outline", "Navigate once and return page headings and landmarks.", outlineToolShape, readOnlyOpenWorld, async (input) => handleOutline(input), outlineOutputSchema); registerJsonTool("browse_find", "Navigate once, search visible text, and return bounded context matches.", findToolShape, readOnlyOpenWorld, async (input) => handleFind(input), findOutputSchema); registerJsonTool("browse_screenshot", "Navigate once and capture a bounded screenshot.", screenshotToolShape, readOnlyOpenWorld, async (input) => handleScreenshot(input)); registerJsonTool("browse_console", "Navigate once and return bounded console diagnostics.", consoleToolShape, readOnlyOpenWorld, async (input) => handleConsole(input)); registerJsonTool("browse_network_summary", "Navigate once and return a bounded network diagnostic summary.", networkSummaryToolShape, readOnlyOpenWorld, async (input) => handleNetworkSummary(input), networkSummaryOutputSchema); registerJsonTool("browse_session_start", "Start an isolated short-lived browser session.", sessionStartToolShape, nonReadOnlyOpenWorld, async (input) => handleSessionStart(input)); registerJsonTool("browse_session_navigate", "Navigate an existing browser session.", sessionNavigateToolShape, nonReadOnlyOpenWorld, async (input) => handleSessionNavigate(input)); registerJsonTool("browse_session_action", "Run one bounded action in an existing browser session.", sessionActionToolShape, nonReadOnlyOpenWorld, async (input) => handleSessionAction(input)); registerJsonTool("browse_session_snapshot", "Read the current state of an existing browser session.", sessionSnapshotToolShape, readOnlyOpenWorld, async (input) => handleSessionSnapshot(input)); registerJsonTool("browse_session_resume", "Resume a paused session after human action and return current state.", sessionResumeToolShape, nonReadOnlyOpenWorld, async (input) => handleSessionResume(input)); registerJsonTool("browse_session_close", "Close an existing browser session.", sessionCloseToolShape, nonReadOnlyOpenWorld, async (input) => handleSessionClose(input)); async function runServer() { try { assertNetworkSandboxPolicy(); 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); } } let shuttingDown = false; async function shutdown(signal, exitCode = 0) { if (shuttingDown) { if (exitCode !== 0) { process.exit(exitCode); } return; } shuttingDown = true; setBrowserShuttingDown(true); console.error(chalk.yellow("\n[Camoufox] Shutting down server after " + signal + "...")); rejectPendingBrowses("Server is shutting down."); try { await closeActiveSessions(); await closeActiveBrowsers(); } catch (shutdownError) { console.error(chalk.red("[Camoufox] Shutdown cleanup failed: " + describeError(shutdownError))); } finally { process.exit(exitCode); } } process.on("SIGINT", () => { void shutdown("SIGINT"); }); process.on("SIGTERM", () => { void shutdown("SIGTERM"); }); process.on("uncaughtException", (error) => { console.error(chalk.red("[Camoufox] Uncaught exception:", error)); void shutdown("uncaughtException", 1); }); process.on("unhandledRejection", (reason, promise) => { console.error(chalk.red("[Camoufox] Unhandled rejection at:", promise, "reason:", reason)); void shutdown("unhandledRejection", 1); }); runServer().catch((error) => { console.error(chalk.red("Fatal error running server:", error)); process.exit(1); });