mirror of
https://github.com/jo-inc/camofox-browser.git
synced 2026-10-02 04:14:41 +08:00
119 lines
4.4 KiB
JavaScript
Executable File
119 lines
4.4 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// camofox-browser MCP server
|
|
//
|
|
// Standalone Model Context Protocol server that exposes the camofox-browser
|
|
// REST API (default http://localhost:9377) as MCP tools. Tool names, schemas,
|
|
// REST routes, request bodies, auth, and response shaping are imported from
|
|
// mcp/lib/tool-contracts.mjs — the SAME source of truth the OpenClaw plugin
|
|
// (plugin.ts) uses — so behavior is identical whether an agent reaches camofox
|
|
// via OpenClaw or MCP. Drift is structurally impossible.
|
|
//
|
|
// Transport: stdio (Claude Code / Codex / agy / Cursor / opencode spawn this as
|
|
// a child process). The camofox REST server itself must be running (npm start) —
|
|
// this server only forwards calls, it does not launch the browser.
|
|
//
|
|
// Auth:
|
|
// - CAMOFOX_ACCESS_KEY (global): forwarded as `Authorization: Bearer` on every
|
|
// request so globally-authenticated REST servers accept MCP traffic.
|
|
// - CAMOFOX_API_KEY (cookie import): forwarded as `Authorization: Bearer` on
|
|
// the cookie-import route only.
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
import { loadMcpConfig } from "./lib/config.mjs";
|
|
import {
|
|
TOOL_DEFS,
|
|
runTool,
|
|
adaptResponse,
|
|
} from "./lib/tool-contracts.mjs";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Version from this package's own package.json (single source — no hardcoded
|
|
// duplicate). mcp/ is an independently installable package (@askjo/camofox-browser-mcp)
|
|
// with its own manifest, so this reads locally rather than the parent repo's.
|
|
const VERSION = JSON.parse(
|
|
readFileSync(join(__dirname, "package.json"), "utf8")
|
|
).version;
|
|
|
|
// Server config (apiKey / accessKey / cookiesDir / port). The standalone
|
|
// package reads only its own environment settings; CAMOFOX_BASE_URL overrides
|
|
// the derived local URL.
|
|
const CONFIG = loadMcpConfig();
|
|
const BASE_URL = process.env.CAMOFOX_BASE_URL || `http://localhost:${CONFIG.port}`;
|
|
|
|
// Per-MCP-server userId so each host session gets an isolated camofox session
|
|
// (cookie/storage partition). Falls back to a random id.
|
|
const USER_ID = process.env.CAMOFOX_USER_ID || `mcp-${randomUUID()}`;
|
|
// sessionKey partitions tabs within a user (matches plugin.ts fallback "default").
|
|
const SESSION_KEY = process.env.CAMOFOX_SESSION_KEY || "default";
|
|
|
|
// The standalone package declares the SDK directly. Surface a clear,
|
|
// actionable error if an incomplete installation is missing it.
|
|
let Server, StdioServerTransport, CallToolRequestSchema, ListToolsRequestSchema;
|
|
try {
|
|
const serverMod = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
Server = serverMod.Server;
|
|
const stdioMod = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
StdioServerTransport = stdioMod.StdioServerTransport;
|
|
const typesMod = await import("@modelcontextprotocol/sdk/types.js");
|
|
CallToolRequestSchema = typesMod.CallToolRequestSchema;
|
|
ListToolsRequestSchema = typesMod.ListToolsRequestSchema;
|
|
} catch {
|
|
console.error(
|
|
"[camofox-browser-mcp] @modelcontextprotocol/sdk is not installed.\n" +
|
|
"Install the adapter dependencies with: npm install\n" +
|
|
"from the @askjo/camofox-browser-mcp package directory."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const server = new Server(
|
|
{ name: "camofox-browser", version: VERSION },
|
|
{ capabilities: { tools: {} } }
|
|
);
|
|
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
tools: TOOL_DEFS.map((t) => ({
|
|
name: t.name,
|
|
description: t.description,
|
|
inputSchema: t.inputSchema,
|
|
})),
|
|
}));
|
|
|
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
const { name, arguments: args } = req.params;
|
|
const def = TOOL_DEFS.find((t) => t.name === name);
|
|
if (!def) {
|
|
return {
|
|
isError: true,
|
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
};
|
|
}
|
|
try {
|
|
const { spec, payload } = await runTool(
|
|
name,
|
|
args || {},
|
|
{ userId: USER_ID, sessionKey: SESSION_KEY },
|
|
BASE_URL,
|
|
CONFIG
|
|
);
|
|
const content = adaptResponse(spec, payload);
|
|
return { content };
|
|
} catch (err) {
|
|
return {
|
|
isError: true,
|
|
content: [{ type: "text", text: `camofox error: ${err.message}` }],
|
|
};
|
|
}
|
|
});
|
|
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
console.error(
|
|
`[camofox-browser-mcp] v${VERSION} connected → ${BASE_URL} (user=${USER_ID})`
|
|
);
|