webclaw/packages/create-webclaw/index.mjs
Valerio d387669c13 refactor(create-webclaw): write the npx @webclaw/mcp config, drop binary download
Aligns with the industry standard: Firecrawl, the official MCP reference servers,
Exa, Brave, Apify, Bright Data, etc. all install their MCP via a single
`npx -y <pkg>` config line — none use a create-* scaffolder for the MCP itself
(create-* is a project-bootstrap convention). create-webclaw now auto-detects AI
tools and writes the canonical `npx @webclaw/mcp` config into each, byte-identical
to webclaw.io/docs and the MCP registries, so a scaffolded config can never
diverge from a hand-written one.

- buildMcpEntry + all writers emit {command:"npx", args:["-y","@webclaw/mcp"]}
  across every format (JSON, Continue array, OpenCode array, Codex TOML)
- remove the entire binary-download machinery (getTarget/download/extract/cargo
  fallback, ~250 lines) — the @webclaw/mcp launcher handles that now
- README: reflect the new behavior + show the one-block manual config
- version 0.1.6 -> 0.1.7

Verified: correct npx config (with/without API key) across all formats;
idempotent re-runs; zero binary-download code remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:34:01 +02:00

380 lines
10 KiB
JavaScript

#!/usr/bin/env node
// create-webclaw — optional convenience installer for the webclaw MCP server.
//
// It auto-detects your AI tools (Claude Desktop, Claude Code, Cursor, Windsurf,
// OpenCode, Codex, Antigravity, ...) and writes the canonical `npx @webclaw/mcp`
// config into each. It does NOT download a binary: the runtime is always the
// `@webclaw/mcp` launcher, so a scaffolded config is byte-identical to what
// webclaw.io/docs and the MCP registries document. If you'd rather not run this,
// just add the one config block below by hand.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { createInterface } from "readline";
import { homedir, platform } from "os";
import { join, dirname } from "path";
// ── Constants ──
const MCP_PACKAGE = "@webclaw/mcp";
const COLORS = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
cyan: "\x1b[36m",
red: "\x1b[31m",
};
const c = (color, text) => `${COLORS[color]}${text}${COLORS.reset}`;
// ── AI Tool Detection ──
const AI_TOOLS = [
{
id: "claude-desktop",
name: "Claude Desktop",
detect: () => {
if (platform() === "darwin")
return existsSync(
join(
homedir(),
"Library/Application Support/Claude/claude_desktop_config.json",
),
);
if (platform() === "win32")
return existsSync(
join(process.env.APPDATA || "", "Claude/claude_desktop_config.json"),
);
return false;
},
configPath: () => {
if (platform() === "darwin")
return join(
homedir(),
"Library/Application Support/Claude/claude_desktop_config.json",
);
if (platform() === "win32")
return join(
process.env.APPDATA || "",
"Claude/claude_desktop_config.json",
);
return null;
},
},
{
id: "claude-code",
name: "Claude Code",
detect: () => existsSync(join(homedir(), ".claude.json")),
configPath: () => join(homedir(), ".claude.json"),
},
{
id: "cursor",
name: "Cursor",
detect: () => {
return (
existsSync(join(homedir(), ".cursor")) ||
existsSync(join(process.cwd(), ".cursor"))
);
},
configPath: () => {
const projectPath = join(process.cwd(), ".cursor", "mcp.json");
const globalPath = join(homedir(), ".cursor", "mcp.json");
return existsSync(join(process.cwd(), ".cursor"))
? projectPath
: globalPath;
},
},
{
id: "windsurf",
name: "Windsurf",
detect: () => {
return (
existsSync(join(homedir(), ".codeium")) ||
existsSync(join(homedir(), ".windsurf"))
);
},
configPath: () =>
join(homedir(), ".codeium", "windsurf", "mcp_config.json"),
},
{
id: "vscode-continue",
name: "VS Code (Continue)",
detect: () => existsSync(join(homedir(), ".continue")),
configPath: () => join(homedir(), ".continue", "config.json"),
},
{
id: "opencode",
name: "OpenCode",
detect: () => {
return (
existsSync(join(homedir(), ".config", "opencode", "opencode.json")) ||
existsSync(join(process.cwd(), "opencode.json"))
);
},
configPath: () => {
const projectPath = join(process.cwd(), "opencode.json");
const globalPath = join(
homedir(),
".config",
"opencode",
"opencode.json",
);
return existsSync(projectPath) ? projectPath : globalPath;
},
},
{
id: "antigravity",
name: "Antigravity",
detect: () => {
return (
existsSync(join(homedir(), ".antigravity")) ||
existsSync(join(homedir(), ".config", "antigravity"))
);
},
configPath: () => {
const configDir = existsSync(join(homedir(), ".config", "antigravity"))
? join(homedir(), ".config", "antigravity")
: join(homedir(), ".antigravity");
return join(configDir, "mcp.json");
},
},
{
id: "codex",
name: "Codex (CLI + App)",
detect: () => existsSync(join(homedir(), ".codex")),
configPath: () => join(homedir(), ".codex", "config.toml"),
},
];
// ── Helpers ──
function ask(question) {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
function readJsonFile(path) {
try {
return JSON.parse(readFileSync(path, "utf-8"));
} catch {
return {};
}
}
function writeJsonFile(path, data) {
const dir = dirname(path);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
}
// The single canonical way to run the webclaw MCP server: the npx launcher.
// `@webclaw/mcp` fetches the prebuilt binary on first run and speaks MCP over
// stdio, so this config matches webclaw.io/docs and the MCP registries exactly.
function buildMcpEntry(apiKey) {
const entry = { command: "npx", args: ["-y", MCP_PACKAGE] };
if (apiKey) entry.env = { WEBCLAW_API_KEY: apiKey };
return entry;
}
const MANUAL_CONFIG = `{ "mcpServers": { "webclaw": { "command": "npx", "args": ["-y", "${MCP_PACKAGE}"] } } }`;
// ── MCP Config Writers ──
function addToMcpServers(configPath, apiKey) {
const config = readJsonFile(configPath);
if (!config.mcpServers) config.mcpServers = {};
config.mcpServers.webclaw = buildMcpEntry(apiKey);
writeJsonFile(configPath, config);
}
function addToVSCodeContinue(configPath, apiKey) {
const config = readJsonFile(configPath);
if (!config.mcpServers) config.mcpServers = [];
// Continue uses array format.
const existing = config.mcpServers.findIndex?.((s) => s.name === "webclaw");
const entry = { name: "webclaw", ...buildMcpEntry(apiKey) };
if (existing >= 0) {
config.mcpServers[existing] = entry;
} else if (Array.isArray(config.mcpServers)) {
config.mcpServers.push(entry);
}
writeJsonFile(configPath, config);
}
function addToOpenCode(configPath, apiKey) {
const config = readJsonFile(configPath);
if (!config.mcp) config.mcp = {};
config.mcp.webclaw = {
type: "local",
command: ["npx", "-y", MCP_PACKAGE],
enabled: true,
};
if (apiKey) {
config.mcp.webclaw.environment = { WEBCLAW_API_KEY: apiKey };
}
writeJsonFile(configPath, config);
}
function addToCodex(configPath, apiKey) {
// Codex uses TOML, not JSON. Replace any existing webclaw section.
const dir = dirname(configPath);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
let existing = "";
try {
existing = readFileSync(configPath, "utf-8");
} catch {
// File doesn't exist yet.
}
existing = existing.replace(
/\n?\[mcp_servers\.webclaw\][^\[]*(?=\[|$)/gs,
"",
);
let section = `\n[mcp_servers.webclaw]\ncommand = "npx"\nargs = ["-y", "${MCP_PACKAGE}"]\nenabled = true\n`;
if (apiKey) {
section += `env = { WEBCLAW_API_KEY = "${apiKey}" }\n`;
}
writeFileSync(configPath, existing.trimEnd() + "\n" + section);
}
const CONFIG_WRITERS = {
"claude-desktop": addToMcpServers,
"claude-code": addToMcpServers,
cursor: addToMcpServers,
windsurf: addToMcpServers,
"vscode-continue": addToVSCodeContinue,
opencode: addToOpenCode,
antigravity: addToMcpServers,
codex: addToCodex,
};
// ── Main ──
async function main() {
console.log();
console.log(c("bold", " ┌─────────────────────────────────────┐"));
console.log(
c("bold", " │") +
c("cyan", " webclaw") +
c("dim", " — MCP setup for AI agents") +
c("bold", " │"),
);
console.log(c("bold", " └─────────────────────────────────────┘"));
console.log();
// 1. Detect installed AI tools.
console.log(c("bold", " Detecting AI tools..."));
console.log();
const detected = AI_TOOLS.filter((tool) => {
try {
return tool.detect();
} catch {
return false;
}
});
if (detected.length === 0) {
console.log(c("yellow", " No supported AI tools detected."));
console.log();
console.log(c("dim", " Add this to your MCP client config by hand:"));
console.log(c("cyan", ` ${MANUAL_CONFIG}`));
console.log();
if (!process.argv.includes("--manual")) {
process.exit(0);
}
}
for (const tool of detected) {
console.log(c("green", `${tool.name}`));
}
console.log();
// 2. Ask for an optional API key.
console.log(
c("dim", " An API key unlocks the cloud tools (bot bypass, JS rendering,"),
);
console.log(
c("dim", " search, research, leads). Without one, webclaw runs locally."),
);
console.log();
const apiKey = await ask(
c("bold", " API key ") +
c("dim", "(press Enter to skip for local-only): "),
);
console.log();
// 3. Write the npx config into each detected tool.
console.log(c("bold", " Writing MCP config..."));
console.log();
for (const tool of detected) {
const configPath = tool.configPath();
if (!configPath) continue;
const writer = CONFIG_WRITERS[tool.id];
if (!writer) continue;
try {
writer(configPath, apiKey || null);
console.log(
c("green", `${tool.name}`) + c("dim", `${configPath}`),
);
} catch (e) {
console.log(c("red", `${tool.name}: ${e.message}`));
}
}
console.log();
// 4. Summary.
console.log(c("bold", " Done! webclaw is configured."));
console.log();
console.log(
c("dim", " Your client runs the server on demand via npx — nothing to"),
);
console.log(
c("dim", " install. First launch fetches @webclaw/mcp, then it's cached."),
);
console.log();
console.log(
c("dim", " Tools: scrape, search, crawl, map, batch, extract, summarize,"),
);
console.log(
c(
"dim",
" diff, brand, research, lead, lead_batch, + 30 site extractors.",
),
);
console.log();
if (!apiKey) {
console.log(c("yellow", " Running in local-only mode (no API key)."));
console.log(
c(
"dim",
" Get a key at https://webclaw.io/dashboard for cloud features.",
),
);
console.log();
}
console.log(c("dim", " Restart your AI tool to activate the MCP server."));
console.log();
}
main().catch((e) => {
console.error(c("red", `\n Error: ${e.message}\n`));
process.exit(1);
});