@kolbo/mcp
Version:
Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code
219 lines (197 loc) • 9.07 kB
JavaScript
;
/**
* `npx @kolbo/mcp install` — one-command, keyless setup.
*
* Detects the MCP config of every supported local agent (Claude Desktop,
* Claude Code, Cursor) that's actually installed, and adds the Kolbo MCP server
* to it — to the RIGHT file, merging instead of clobbering. No API key (the
* server logs in via the browser on first use). This exists because having an
* agent hand-edit settings.json is fragile (wrong file, JSON breakage, and
* Claude Code's self-modification guard) — one deterministic command is not.
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const KOLBO_ENTRY = { command: 'npx', args: ['-y', '@kolbo/mcp@latest'] };
const MANAGED_FILE = '.kolbo-managed.json';
const PACKAGE_VERSION = require('../package.json').version;
function targets() {
const home = os.homedir();
let desktop;
if (process.platform === 'darwin') {
desktop = path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
} else if (process.platform === 'win32') {
desktop = path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
} else {
desktop = path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
}
return [
{ name: 'Claude Desktop', file: desktop, restart: 'Fully quit and reopen Claude Desktop' },
// Claude Code reads user-scope MCP servers from ~/.claude.json — NOT from
// ~/.claude/settings.json. settings.json accepts an mcpServers key without
// complaint and Claude Code ignores it completely, so writing there made
// `install` report success while the user got no Kolbo tools at all.
// Verified with `claude mcp list`: only ~/.claude.json entries load.
{ name: 'Claude Code', file: path.join(home, '.claude.json'), restart: 'Restart Claude Code', probeDir: path.join(home, '.claude') },
{ name: 'Cursor', file: path.join(home, '.cursor', 'mcp.json'), restart: 'Restart Cursor' },
];
}
function configure(t) {
const dir = path.dirname(t.file);
const fileExists = fs.existsSync(t.file);
// Only touch an app that looks installed (its config file or parent dir exists)
// so we don't create configs for apps the user doesn't have. `probeDir` exists
// for configs that live directly in $HOME (~/.claude.json) — dirname there is
// the home dir, which always exists, so it would "detect" every app.
const probe = t.probeDir || dir;
if (!fileExists && !fs.existsSync(probe)) return { ...t, status: 'not found' };
let cfg = {};
if (fileExists) {
try {
cfg = JSON.parse(fs.readFileSync(t.file, 'utf8'));
} catch (_) {
return { ...t, status: 'skipped — existing config is not valid JSON' };
}
}
cfg.mcpServers = cfg.mcpServers || {};
if (cfg.mcpServers.kolbo) return { ...t, status: 'already set up' };
cfg.mcpServers.kolbo = { ...KOLBO_ENTRY };
try {
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(t.file, JSON.stringify(cfg, null, 2) + '\n');
return { ...t, status: 'configured' };
} catch (e) {
return { ...t, status: `failed — ${e.message}` };
}
}
// The Kolbo routing skill is bundled at <package>/skill/ — copy it into the
// agent's skills dir so the agent gets the routing brain (correct model ids,
// defaults, "never hardcode model names"), not just the raw tools.
function skillTargets() {
const home = os.homedir();
return [
{ name: 'Claude Code skill', root: path.join(home, '.claude'), dir: path.join(home, '.claude', 'skills', 'kolbo') },
{ name: 'Agents skill (Cursor/Codex)', root: path.join(home, '.agents'), dir: path.join(home, '.agents', 'skills', 'kolbo') },
{ name: 'Kolbo Code skill', root: path.join(home, '.kolbo'), dir: path.join(home, '.kolbo', 'skills', 'kolbo') },
];
}
function installSkill(t) {
const src = path.join(__dirname, '..', 'skill');
if (!fs.existsSync(src)) return { ...t, status: 'skill not bundled' };
if (!fs.existsSync(t.root)) return { ...t, status: 'not found' };
const tmp = `${t.dir}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
try {
// Replace the generated tree so source deletions propagate too. A simple
// recursive copy leaves obsolete references behind indefinitely.
fs.rmSync(tmp, { recursive: true, force: true });
fs.mkdirSync(path.dirname(t.dir), { recursive: true });
fs.cpSync(src, tmp, { recursive: true });
fs.writeFileSync(path.join(tmp, MANAGED_FILE), JSON.stringify({
source: '@kolbo/mcp',
skill: 'kolbo',
version: fs.readFileSync(path.join(src, 'VERSION'), 'utf8').trim(),
packageVersion: PACKAGE_VERSION,
}, null, 2) + '\n');
fs.rmSync(t.dir, { recursive: true, force: true });
fs.renameSync(tmp, t.dir);
return { ...t, status: 'installed' };
} catch (e) {
fs.rmSync(tmp, { recursive: true, force: true });
return { ...t, status: `failed — ${e.message}` };
}
}
// Local MCP launches refresh only installs created by Kolbo. A manually
// authored `kolbo` skill without the marker is never overwritten.
function refreshManagedSkills() {
return skillTargets().map((t) => {
const marker = path.join(t.dir, MANAGED_FILE);
if (!fs.existsSync(marker)) return { ...t, status: 'unmanaged' };
try {
const installed = JSON.parse(fs.readFileSync(marker, 'utf8'));
const skillVersion = fs.readFileSync(path.join(__dirname, '..', 'skill', 'VERSION'), 'utf8').trim();
if (installed.packageVersion === PACKAGE_VERSION && installed.version === skillVersion) {
return { ...t, status: 'current' };
}
} catch (_) {}
return installSkill(t);
});
}
// Versions before this fix wrote the Claude Code entry into
// ~/.claude/settings.json, where Claude Code silently ignores it. Anyone who ran
// those is left with a dead entry that makes it look configured. Detect and say
// so rather than editing a file we no longer own.
function staleClaudeCodeEntry() {
const f = path.join(os.homedir(), '.claude', 'settings.json');
try {
const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
return cfg.mcpServers && cfg.mcpServers.kolbo ? f : null;
} catch (_) {
return null;
}
}
async function run() {
const out = (s = '') => process.stdout.write(s + '\n');
const results = targets().map(configure);
const skills = skillTargets().map(installSkill);
const newlyConfigured = results.filter((r) => r.status === 'configured');
const ready = results.filter((r) => r.status === 'configured' || r.status === 'already set up');
out();
out(' Kolbo MCP — keyless setup');
out(' ─────────────────────────');
for (const r of results) {
const ok = r.status === 'configured' || r.status === 'already set up';
out(` ${ok ? '✓' : '·'} ${r.name}: ${r.status}${ok ? ` (${r.file})` : ''}`);
}
for (const s of skills) {
if (s.status === 'not found' || s.status === 'skill not bundled') continue;
out(` ${s.status === 'installed' ? '✓' : '·'} ${s.name}: ${s.status}`);
}
out();
if (ready.length === 0) {
out(' No supported agent found (Claude Desktop / Claude Code / Cursor).');
out(' Add this to your agent\'s MCP config manually, then restart it:');
out(' {"mcpServers":{"kolbo":{"command":"npx","args":["-y","@kolbo/mcp@latest"]}}}');
out();
return 0;
}
const stale = staleClaudeCodeEntry();
if (stale) {
out(' Note: an older Kolbo entry is sitting in');
out(` ${stale}`);
out(' Claude Code does not read MCP servers from that file, so it never did');
out(' anything. Safe to delete the "kolbo" key there; the real one is now in');
out(` ${path.join(os.homedir(), '.claude.json')}`);
out();
}
out(' Done — no API key needed.');
if (newlyConfigured.length) {
const steps = [...new Set(newlyConfigured.map((r) => r.restart))];
out(` Next: ${steps.join('; ')}.`);
}
out(' On your first Kolbo generation, a login opens in your browser — click Allow.');
out();
return 0;
}
async function runSkillOnly() {
const out = (s = '') => process.stdout.write(s + '\n');
const skills = skillTargets().map(installSkill);
const ready = skills.filter((s) => s.status === 'installed');
out();
out(' Kolbo Skill — standalone install');
out(' ────────────────────────────────');
for (const s of skills) {
out(` ${s.status === 'installed' ? '✓' : '·'} ${s.name}: ${s.status}`);
}
out();
if (ready.length === 0) {
out(' No supported skill directory found (.claude or .agents).');
out(' Install or open a compatible agent, then run this command again.');
out();
return 0;
}
out(' Done — the Kolbo Skill is installed.');
out(' MCP settings were not changed. Restart your agent to load the skill.');
out();
return 0;
}
module.exports = { run, runSkillOnly, refreshManagedSkills, MANAGED_FILE };