kuzu-mcp-server
Version:
A Model Context Protocol (MCP) server for Kuzu graph databases. Enables LLMs like Claude to execute Cypher queries and analyze graph data with path invariance support.
394 lines (333 loc) • 10.5 kB
JavaScript
/**
* kuzu-mcp-server - Executable bin script for npx usage
*
* This script provides a command-line interface for the Kuzu MCP server,
* allowing users to run it directly with npx kuzu-mcp-server
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
// ANSI color codes for better output
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function logError(message) {
log(`❌ ${message}`, 'red');
}
function logSuccess(message) {
log(`✅ ${message}`, 'green');
}
function logInfo(message) {
log(`ℹ️ ${message}`, 'blue');
}
function logWarning(message) {
log(`⚠️ ${message}`, 'yellow');
}
function showHelp() {
log('Kuzu MCP Server', 'cyan');
log('===============', 'cyan');
console.log();
log('A Model Context Protocol server for Kuzu graph databases');
console.log();
log('Usage:', 'yellow');
console.log(' npx kuzu-mcp-server [options] [database-path]');
console.log(' kuzu-mcp-server [options] [database-path]');
console.log();
log('Options:', 'yellow');
console.log(' -h, --help Show this help message');
console.log(' -v, --version Show version information');
console.log(' --setup Initialize a new database with sample data');
console.log(' --test Run the test suite');
console.log(' --health Run health check');
console.log(' --read-only Start in read-only mode');
console.log(' --port <port> Port for HTTP mode (not implemented)');
console.log();
log('Examples:', 'yellow');
console.log(' npx kuzu-mcp-server ./my-database');
console.log(' npx kuzu-mcp-server --setup ./new-database');
console.log(' npx kuzu-mcp-server --test');
console.log(' npx kuzu-mcp-server --read-only ./production-db');
console.log();
log('Environment Variables:', 'yellow');
console.log(' KUZU_DB_PATH Default database path');
console.log(' KUZU_READ_ONLY Set to "true" for read-only mode');
console.log();
log('For more information, visit:', 'cyan');
console.log(' https://github.com/bmorphism/kuzu-mcp-server');
}
function showVersion() {
const packageJson = require('../package.json');
log(`Kuzu MCP Server v${packageJson.version}`, 'cyan');
console.log(`Author: ${packageJson.author.name} <${packageJson.author.email}>`);
console.log(`License: ${packageJson.license}`);
}
async function runSetup(dbPath) {
const setupScript = path.join(__dirname, '..', 'setup-with-callback.js');
if (!fs.existsSync(setupScript)) {
logError('Setup script not found');
process.exit(1);
}
logInfo(`Setting up database at: ${dbPath}`);
return new Promise((resolve, reject) => {
const child = spawn('node', [setupScript, dbPath], {
stdio: 'inherit'
});
child.on('close', (code) => {
if (code === 0) {
logSuccess('Database setup completed successfully');
resolve();
} else {
logError(`Setup failed with exit code ${code}`);
reject(new Error(`Setup failed with exit code ${code}`));
}
});
child.on('error', (error) => {
logError(`Setup failed: ${error.message}`);
reject(error);
});
});
}
async function runTest() {
const testScript = path.join(__dirname, '..', 'test-mcp.js');
if (!fs.existsSync(testScript)) {
logError('Test script not found');
process.exit(1);
}
logInfo('Running comprehensive test suite...');
return new Promise((resolve, reject) => {
const child = spawn('node', [testScript], {
stdio: 'inherit'
});
child.on('close', (code) => {
if (code === 0) {
logSuccess('All tests passed');
resolve();
} else {
logError(`Tests failed with exit code ${code}`);
reject(new Error(`Tests failed with exit code ${code}`));
}
});
child.on('error', (error) => {
logError(`Test execution failed: ${error.message}`);
reject(error);
});
});
}
async function runHealthCheck(dbPath) {
const healthCheckRequest = {
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {
name: 'healthCheck',
arguments: {}
}
};
const serverScript = path.join(__dirname, '..', 'index.js');
logInfo(`Running health check on database: ${dbPath}`);
return new Promise((resolve, reject) => {
const child = spawn('node', [serverScript, dbPath], {
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
let jsonResponse = '';
const timeout = setTimeout(() => {
child.kill();
reject(new Error('Health check timeout'));
}, 10000);
child.stdout.on('data', (data) => {
const output = data.toString();
stdout += output;
// Look for JSON response
const lines = output.split('\n');
for (const line of lines) {
if (line.trim().startsWith('{')) {
jsonResponse += line.trim();
}
}
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
clearTimeout(timeout);
if (jsonResponse) {
try {
const response = JSON.parse(jsonResponse);
if (response.result && response.result.content) {
const healthData = JSON.parse(response.result.content[0].text);
if (healthData.status === 'healthy') {
logSuccess('Database is healthy');
console.log('Health Status:', JSON.stringify(healthData, null, 2));
resolve();
} else {
logError('Database is unhealthy');
console.log('Health Status:', JSON.stringify(healthData, null, 2));
reject(new Error('Database is unhealthy'));
}
} else {
logError('Invalid health check response');
reject(new Error('Invalid health check response'));
}
} catch (parseError) {
logError(`Failed to parse health check response: ${parseError.message}`);
reject(parseError);
}
} else {
logError('No response from health check');
reject(new Error('No response from health check'));
}
});
child.on('error', (error) => {
clearTimeout(timeout);
logError(`Health check failed: ${error.message}`);
reject(error);
});
// Send the health check request
child.stdin.write(JSON.stringify(healthCheckRequest) + '\n');
child.stdin.end();
});
}
function startServer(dbPath, options = {}) {
const serverScript = path.join(__dirname, '..', 'index.js');
const args = [serverScript];
if (dbPath) {
args.push(dbPath);
}
// Set environment variables based on options
const env = { ...process.env };
if (options.readOnly) {
env.KUZU_READ_ONLY = 'true';
}
if (dbPath) {
env.KUZU_DB_PATH = dbPath;
}
logInfo(`Starting Kuzu MCP Server...`);
logInfo(`Database path: ${dbPath || env.KUZU_DB_PATH || 'default'}`);
logInfo(`Read-only mode: ${options.readOnly ? 'enabled' : 'disabled'}`);
const child = spawn('node', args, {
stdio: 'inherit',
env: env
});
child.on('error', (error) => {
logError(`Failed to start server: ${error.message}`);
process.exit(1);
});
// Handle graceful shutdown
process.on('SIGINT', () => {
logInfo('Shutting down server...');
child.kill('SIGINT');
});
process.on('SIGTERM', () => {
logInfo('Shutting down server...');
child.kill('SIGTERM');
});
}
async function main() {
const args = process.argv.slice(2);
let dbPath = null;
const options = {
readOnly: false
};
// Parse command line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '-h':
case '--help':
showHelp();
process.exit(0);
break;
case '-v':
case '--version':
showVersion();
process.exit(0);
break;
case '--setup':
// Next argument should be database path
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
dbPath = args[i + 1];
i++; // Skip next argument as it's the path
} else {
dbPath = './kuzu_data';
}
try {
await runSetup(dbPath);
process.exit(0);
} catch (error) {
process.exit(1);
}
break;
case '--test':
try {
await runTest();
process.exit(0);
} catch (error) {
process.exit(1);
}
break;
case '--health':
// Next argument should be database path
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
dbPath = args[i + 1];
i++; // Skip next argument as it's the path
} else {
dbPath = process.env.KUZU_DB_PATH || './kuzu_data';
}
try {
await runHealthCheck(dbPath);
process.exit(0);
} catch (error) {
process.exit(1);
}
break;
case '--read-only':
options.readOnly = true;
break;
case '--port':
// Skip port argument for now (not implemented)
if (i + 1 < args.length) {
i++; // Skip next argument
}
logWarning('HTTP port mode not yet implemented');
break;
default:
// Assume it's a database path if it doesn't start with --
if (!arg.startsWith('-')) {
dbPath = arg;
} else {
logError(`Unknown option: ${arg}`);
showHelp();
process.exit(1);
}
break;
}
}
// If no specific action was requested, start the server
startServer(dbPath, options);
}
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
logError(`Uncaught exception: ${error.message}`);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
logError(`Unhandled rejection at: ${promise}, reason: ${reason}`);
process.exit(1);
});
// Run the main function
main().catch((error) => {
logError(`Application error: ${error.message}`);
process.exit(1);
});