UNPKG

@viberaven/mcp

Version:

MCP server for VibeRaven production-readiness: check_readiness, verify, heal, audit, and strict gate tools for Cursor, Claude Code, and Codex.

408 lines (371 loc) 11.6 kB
#!/usr/bin/env node const { spawn } = require('node:child_process'); let spawnChild = spawn; const SERVER_INFO = { name: 'viberaven-mcp', version: '1.1.7' }; const CLI_PACKAGE = '@viberaven/cli'; const cwdSchema = { type: 'object', properties: { cwd: { type: 'string', description: 'Project root. Defaults to the MCP server working directory.' } }, additionalProperties: false }; const healSchema = { type: 'object', properties: { cwd: { type: 'string' }, target: { type: 'string' }, gap: { type: 'string' }, yes: { type: 'boolean' } }, additionalProperties: false }; const TOOLS = [ { name: 'viberaven_check_readiness', description: 'Run the VibeRaven agent-mode production-readiness check and write .viberaven artifacts.', inputSchema: cwdSchema }, { name: 'viberaven_verify', description: 'Rescan and refresh VibeRaven production-readiness artifacts after a fix.', inputSchema: cwdSchema }, { name: 'viberaven_audit', description: 'Run local Vercel/Supabase production checks for RLS, service-role boundaries, and Vercel pooler usage.', inputSchema: { type: 'object', properties: { cwd: { type: 'string', description: 'Project root. Defaults to current working directory.' }, json: { type: 'boolean', description: 'Return JSON output.' } }, additionalProperties: false } }, { name: 'viberaven_init_rules', description: 'Install bounded VibeRaven rules into native AI instruction files.', inputSchema: { type: 'object', properties: { cwd: { type: 'string', description: 'Project root. Defaults to current working directory.' }, agents: { type: 'string', description: 'Comma-separated agent targets, or all.' }, dryRun: { type: 'boolean', description: 'Preview changes without writing files.' } }, additionalProperties: false } }, { name: 'viberaven_clean_plan', description: 'Write a non-destructive context cleanup plan for generated artifacts and logs.', inputSchema: cwdSchema }, { name: 'viberaven_strict_gate', description: 'Run VibeRaven agent-mode strict gate and return the machine verdict.', inputSchema: cwdSchema }, { name: 'viberaven_gate_result', description: 'Run VibeRaven agent-mode JSON output and return gate-result.json content.', inputSchema: cwdSchema }, { name: 'viberaven_context_map', description: 'Refresh .viberaven/context-map.json from the last scan.', inputSchema: cwdSchema }, { name: 'viberaven_actions', description: 'Read the current VibeRaven chat-native action surface from .viberaven/actions.json.', inputSchema: cwdSchema }, { name: 'viberaven_verify_action', description: 'Verify or explain the lifecycle state for a stable VibeRaven action ID.', inputSchema: { type: 'object', properties: { cwd: { type: 'string' }, actionId: { type: 'string' } }, required: ['actionId'], additionalProperties: false } }, { name: 'viberaven_heal_plan', description: 'Write a non-destructive VibeRaven heal plan for a target file or gap.', inputSchema: healSchema }, { name: 'viberaven_heal_prompt', description: 'Write an agent-ready VibeRaven heal prompt for a target file or gap.', inputSchema: healSchema }, { name: 'viberaven_heal_apply', description: 'Apply a guarded VibeRaven repo-code heal recipe when supported.', inputSchema: healSchema }, { name: 'viberaven_validate_npm_package', description: 'Check whether an npm package name exists and looks safe before install. Always run viberaven --agent-mode after adding dependencies.', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'Single npm package name to validate.' }, packageNames: { type: 'array', items: { type: 'string' }, description: 'Multiple npm package names to validate in one call.' } }, additionalProperties: false } } ]; let buffer = Buffer.alloc(0); function main() { if (process.argv.includes('--help') || process.argv.includes('-h')) { console.log(`${SERVER_INFO.name} ${SERVER_INFO.version}`); console.log(`MCP stdio server wrapping npx -y ${CLI_PACKAGE}.`); process.exit(0); } process.stdin.on('data', (chunk) => { buffer = Buffer.concat([buffer, chunk]); drainMessages(); }); } function drainMessages() { while (true) { const headerEnd = buffer.indexOf('\r\n\r\n'); if (headerEnd === -1) return; const header = buffer.slice(0, headerEnd).toString('utf8'); const match = header.match(/content-length:\s*(\d+)/i); if (!match) { buffer = buffer.slice(headerEnd + 4); continue; } const length = Number(match[1]); const messageStart = headerEnd + 4; const messageEnd = messageStart + length; if (buffer.length < messageEnd) return; const raw = buffer.slice(messageStart, messageEnd).toString('utf8'); buffer = buffer.slice(messageEnd); void handleRawMessage(raw); } } async function handleRawMessage(raw) { let message; try { message = JSON.parse(raw); } catch { return; } if (!message || typeof message.method !== 'string') return; if (message.id === undefined || message.id === null) return; try { const result = await handleRequest(message.method, message.params || {}); send({ jsonrpc: '2.0', id: message.id, result }); } catch (error) { send({ jsonrpc: '2.0', id: message.id, error: { code: -32000, message: error instanceof Error ? error.message : String(error) } }); } } async function handleRequest(method, params) { if (method === 'initialize') { return { protocolVersion: params.protocolVersion || '2024-11-05', capabilities: { tools: {} }, serverInfo: SERVER_INFO }; } if (method === 'tools/list') { return { tools: TOOLS }; } if (method === 'tools/call') { const name = params.name; const args = params.arguments || {}; const text = await callTool(name, args); const parsed = parseJsonObjectFromToolText(text); return { content: [{ type: 'text', text }], ...(parsed ? { structuredContent: parsed } : {}) }; } throw new Error(`Unsupported MCP method: ${method}`); } async function callTool(name, args) { return runVibeRaven(['-y', CLI_PACKAGE, ...buildToolArgs(name, args || {})], args && args.cwd); } function buildToolArgs(name, args) { if (name === 'viberaven_check_readiness') { return ['--agent-mode']; } if (name === 'viberaven_verify') { return ['--verify']; } if (name === 'viberaven_audit') { return ['audit', '--vercel-supabase', ...(args.json ? ['--json'] : [])]; } if (name === 'viberaven_init_rules') { const cliArgs = ['init']; if (args.agents) cliArgs.push('--agents', safeStringArg(args.agents, 'agents')); if (args.dryRun) cliArgs.push('--dry-run'); return cliArgs; } if (name === 'viberaven_clean_plan') { return ['clean', '--plan']; } if (name === 'viberaven_strict_gate') { return ['--agent-mode', '--strict', '--json']; } if (name === 'viberaven_gate_result') { return ['--agent-mode', '--json']; } if (name === 'viberaven_context_map') { return ['--condense']; } if (name === 'viberaven_actions') { return ['actions']; } if (name === 'viberaven_verify_action') { return ['--verify', '--action', safeStringArg(args.actionId, 'actionId')]; } if (name === 'viberaven_heal_plan') { return buildHealArgs(['--heal', '--plan'], args); } if (name === 'viberaven_heal_prompt') { return buildHealArgs(['--heal', '--prompt'], args); } if (name === 'viberaven_heal_apply') { const cliArgs = buildHealArgs(['--heal', '--apply'], args); if (args.yes === true) cliArgs.push('--yes'); return cliArgs; } if (name === 'viberaven_validate_npm_package') { const names = []; if (args.packageName) { names.push(safeStringArg(args.packageName, 'packageName')); } if (Array.isArray(args.packageNames)) { for (const entry of args.packageNames) { names.push(safeStringArg(entry, 'packageName')); } } if (names.length === 0) { throw new Error('packageName or packageNames is required'); } return ['validate-npm-package', '--json', ...names]; } throw new Error(`Unknown VibeRaven tool: ${name}`); } function buildHealArgs(prefix, args) { const cliArgs = [...prefix]; if (args.target) cliArgs.push('--target', safeStringArg(args.target, 'target')); if (args.gap) cliArgs.push('--gap', safeStringArg(args.gap, 'gap')); return cliArgs; } function safeStringArg(value, label) { if (typeof value !== 'string') { throw new Error(`Invalid ${label}: expected string`); } if (!value.trim()) { throw new Error(`Invalid ${label}: expected non-empty string`); } if (/[&|<>^%!"\r\n]/.test(value)) { throw new Error(`Unsafe characters in ${label}`); } return value; } function resolveCwd(cwd) { if (cwd === undefined || cwd === null) { return process.cwd(); } if (typeof cwd !== 'string') { throw new Error('Invalid cwd: expected string'); } if (!cwd.trim()) { throw new Error('Invalid cwd: expected non-empty string'); } return cwd; } function buildNpxCommand(args) { if (process.platform === 'win32') { return { command: 'cmd.exe', args: ['/d', '/s', '/c', 'npx', ...args], options: { shell: false } }; } return { command: 'npx', args, options: { shell: false } }; } function runVibeRaven(args, cwd) { return new Promise((resolve) => { const built = buildNpxCommand(args); const child = spawnChild(built.command, built.args, { cwd: resolveCwd(cwd), env: process.env, shell: built.options.shell, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); child.stderr.on('data', (chunk) => { stderr += chunk.toString(); }); child.on('error', (error) => { resolve(`ERROR: ${error.message}`); }); child.on('close', (code) => { const combined = `${stdout}${stderr}`.trim(); resolve(`exit ${code ?? 1}${combined ? `\n${combined}` : ''}`); }); }); } function parseJsonObjectFromToolText(text) { const jsonStart = text.indexOf('{'); if (jsonStart === -1) return undefined; try { return JSON.parse(text.slice(jsonStart)); } catch { return undefined; } } function send(message) { const body = JSON.stringify(message); process.stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`); } if (require.main === module) { main(); } module.exports = { TOOLS, buildNpxCommand, callTool, handleRequest, parseJsonObjectFromToolText, safeStringArg, __setSpawnForTest(spawnForTest) { spawnChild = spawnForTest; }, __resetSpawnForTest() { spawnChild = spawn; } };