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
JavaScript
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);
});