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