UNPKG

controlai-mcp

Version:

High-Performance Enterprise AI Project Management MCP Server with advanced optimization and multi-agent coordination

589 lines (588 loc) 20.1 kB
import { CBDDatabaseAdapter } from './CBDDatabaseAdapter.js'; import { Logger } from '../utils/logger.js'; import { spawn } from 'child_process'; import { join } from 'path'; import { fileURLToPath } from 'url'; import { TaskStatus } from '../types/index.js'; /** * Local in-memory database for fallback when CBD is not available */ class LocalMemoryDatabase { projects = new Map(); tasks = new Map(); agents = new Map(); async createProject(project) { this.projects.set(project.id, project); return project; } async getProject(id) { return this.projects.get(id) || null; } async createTask(task) { this.tasks.set(task.id, task); return task; } async getTask(id) { return this.tasks.get(id) || null; } async createAgent(agent) { this.agents.set(agent.id, agent); return agent; } async getAgent(id) { return this.agents.get(id) || null; } async getProjectTasks(projectId) { return Array.from(this.tasks.values()).filter(task => task.projectId === projectId); } async getAgents() { return Array.from(this.agents.values()); } async updateTask(task) { this.tasks.set(task.id, task); return task; } async getProjects() { return Array.from(this.projects.values()); } async deleteProject(id) { return this.projects.delete(id); } async deleteTask(id) { return this.tasks.delete(id); } async deleteAgent(id) { return this.agents.delete(id); } async updateProject(project) { this.projects.set(project.id, project); return project; } async updateAgent(agent) { this.agents.set(agent.id, agent); return agent; } // Helper method to get tasks for local search getTasks() { return Array.from(this.tasks.values()); } } /** * Database Service for ControlAI MCP * * Automatically manages CBD service startup and connectivity. * Falls back to local in-memory storage if CBD unavailable. */ export class DatabaseService { adapter; localDb; isInitialized = false; cbdProcess = null; maxRetries = 3; retryDelay = 2000; // 2 seconds useCBD = false; constructor(cbdUrl) { this.adapter = new CBDDatabaseAdapter(cbdUrl); this.localDb = new LocalMemoryDatabase(); } async initialize() { try { Logger.debug('[ControlAI] Initializing database connection...'); // First, try to connect to existing CBD service let isConnected = await this.adapter.testConnection(); if (!isConnected) { Logger.debug('[ControlAI] CBD service not available, attempting to start local service...'); // Try to start local CBD service const serviceStarted = await this.startCBDService(); if (serviceStarted) { // Wait a moment for service to initialize await this.sleep(3000); // Retry connection with backoff isConnected = await this.retryConnection(); } else { console.log('[ControlAI] Failed to start CBD service, using local fallback mode'); } } if (isConnected) { console.log('[ControlAI] CBD database connection established successfully'); this.useCBD = true; } else { Logger.debug('[ControlAI] Using local in-memory database (fallback mode)'); this.useCBD = false; } this.isInitialized = true; } catch (error) { console.warn('[ControlAI] Database initialization warning:', error); console.log('[ControlAI] Continuing with local in-memory database'); this.useCBD = false; this.isInitialized = true; } } async startCBDService() { try { const cbdPackagePath = process.env.CBD_PACKAGE_PATH || this.findCBDPackage(); if (!cbdPackagePath) { console.log('[ControlAI] CBD package not found locally, checking npm...'); return await this.startCBDViaNPM(); } console.log('[ControlAI] Starting local CBD service...'); this.cbdProcess = spawn('npm', ['run', 'service'], { cwd: cbdPackagePath, stdio: ['ignore', 'pipe', 'pipe'], shell: true, detached: false }); if (this.cbdProcess.stdout) { this.cbdProcess.stdout.on('data', (data) => { console.log(`[CBD] ${data.toString().trim()}`); }); } if (this.cbdProcess.stderr) { this.cbdProcess.stderr.on('data', (data) => { console.error(`[CBD] ${data.toString().trim()}`); }); } this.cbdProcess.on('exit', (code) => { console.log(`[CBD] Service exited with code ${code}`); this.cbdProcess = null; }); return true; } catch (error) { console.error('[ControlAI] Failed to start CBD service:', error); return false; } } async startCBDViaNPM() { try { console.log('[ControlAI] Starting CBD service via npm...'); this.cbdProcess = spawn('npx', ['cbd-service'], { stdio: ['ignore', 'pipe', 'pipe'], shell: true, detached: false }); if (this.cbdProcess.stdout) { this.cbdProcess.stdout.on('data', (data) => { console.log(`[CBD-NPX] ${data.toString().trim()}`); }); } if (this.cbdProcess.stderr) { this.cbdProcess.stderr.on('data', (data) => { console.error(`[CBD-NPX] ${data.toString().trim()}`); }); } return true; } catch (error) { console.error('[ControlAI] Failed to start CBD via npm:', error); return false; } } findCBDPackage() { try { // Get __dirname equivalent for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = join(__filename, '..'); const possiblePaths = [ join(process.cwd(), '..', '..', 'packages', 'cbd'), join(process.cwd(), '..', 'packages', 'cbd'), join(process.cwd(), 'packages', 'cbd'), join(__dirname, '..', '..', '..', 'cbd'), join(__dirname, '..', '..', '..', '..', 'cbd'), process.env.CBD_PACKAGE_PATH ].filter(Boolean); for (const path of possiblePaths) { try { const packageJsonPath = join(path, 'package.json'); const fs = require('fs'); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); if (packageJson.name === '@codai/cbd') { console.log(`[ControlAI] Found CBD package at: ${path}`); return path; } } } catch (err) { // Continue searching } } return null; } catch (error) { console.error('[ControlAI] Error finding CBD package:', error); return null; } } async retryConnection() { for (let i = 0; i < this.maxRetries; i++) { try { console.log(`[ControlAI] Attempting connection retry ${i + 1}/${this.maxRetries}...`); const isConnected = await this.adapter.testConnection(); if (isConnected) { console.log('[ControlAI] CBD connection established on retry'); return true; } if (i < this.maxRetries - 1) { await this.sleep(this.retryDelay * (i + 1)); } } catch (error) { console.log(`[ControlAI] Retry ${i + 1} failed:`, error); } } return false; } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async cleanup() { try { if (this.cbdProcess) { console.log('[ControlAI] Shutting down CBD service...'); this.cbdProcess.kill('SIGTERM'); await this.sleep(2000); if (this.cbdProcess && !this.cbdProcess.killed) { this.cbdProcess.kill('SIGKILL'); } this.cbdProcess = null; } } catch (error) { console.error('[ControlAI] Error during cleanup:', error); } } // ======================================== // PROJECT MANAGEMENT METHODS WITH FALLBACK // ======================================== async createProject(project) { this.ensureInitialized(); const fullProject = { ...project, createdAt: new Date(), updatedAt: new Date() }; if (this.useCBD) { try { return await this.adapter.createProject(fullProject); } catch (error) { console.log('[ControlAI] CBD adapter failed, falling back to local database:', error); return this.localDb.createProject(fullProject); } } else { return this.localDb.createProject(fullProject); } } async getProject(id) { this.ensureInitialized(); if (this.useCBD) { try { return await this.adapter.getProject(id); } catch (error) { console.log('[ControlAI] CBD adapter failed, falling back to local database:', error); return this.localDb.getProject(id); } } else { return this.localDb.getProject(id); } } async getAllProjects() { this.ensureInitialized(); if (this.useCBD) { try { return await this.adapter.getAllProjects(); } catch (error) { console.log('[ControlAI] CBD adapter failed, falling back to local database:', error); return this.localDb.getProjects(); } } else { return this.localDb.getProjects(); } } async updateProject(id, updates) { this.ensureInitialized(); const updatedProject = { ...updates, id, updatedAt: new Date() }; if (this.useCBD) { return await this.adapter.updateProject(id, updates); } else { const existing = await this.localDb.getProject(id); if (!existing) { throw new Error(`Project ${id} not found`); } const merged = { ...existing, ...updatedProject }; return this.localDb.updateProject(merged); } } async deleteProject(id) { this.ensureInitialized(); if (this.useCBD) { return await this.adapter.deleteProject(id); } else { return await this.localDb.deleteProject(id); } } // ======================================== // TASK MANAGEMENT METHODS WITH FALLBACK // ======================================== async createTask(task) { this.ensureInitialized(); const fullTask = { ...task, createdAt: new Date(), updatedAt: new Date() }; if (this.useCBD) { return await this.adapter.createTask(fullTask); } else { return this.localDb.createTask(fullTask); } } async getTask(id) { this.ensureInitialized(); if (this.useCBD) { return await this.adapter.getTask(id); } else { return this.localDb.getTask(id); } } async getTasksByProject(projectId) { this.ensureInitialized(); if (this.useCBD) { return await this.adapter.getTasksByProject(projectId); } else { return this.localDb.getProjectTasks(projectId); } } async getAvailableTasks() { this.ensureInitialized(); if (this.useCBD) { return await this.adapter.getAvailableTasks(); } else { const allTasks = this.localDb.getTasks(); return allTasks.filter(task => task.status === TaskStatus.TODO || task.status === TaskStatus.ASSIGNED); } } async updateTask(id, updates) { this.ensureInitialized(); const updatedTask = { ...updates, id, updatedAt: new Date() }; if (this.useCBD) { return await this.adapter.updateTask(id, updates); } else { const existing = await this.localDb.getTask(id); if (!existing) { throw new Error(`Task ${id} not found`); } const merged = { ...existing, ...updatedTask }; return this.localDb.updateTask(merged); } } async deleteTask(id) { this.ensureInitialized(); if (this.useCBD) { return await this.adapter.deleteTask(id); } else { return await this.localDb.deleteTask(id); } } // ======================================== // AGENT MANAGEMENT METHODS WITH FALLBACK // ======================================== async registerAgent(agent) { this.ensureInitialized(); const fullAgent = { ...agent, createdAt: new Date(), lastActiveAt: new Date() }; if (this.useCBD) { return await this.adapter.registerAgent(fullAgent); } else { return this.localDb.createAgent(fullAgent); } } async getAgent(id) { this.ensureInitialized(); if (this.useCBD) { return await this.adapter.getAgent(id); } else { return this.localDb.getAgent(id); } } async getAllAgents() { this.ensureInitialized(); if (this.useCBD) { try { return await this.adapter.getAllAgents(); } catch (error) { console.log('[ControlAI] CBD adapter failed, falling back to local database:', error); return this.localDb.getAgents(); } } else { return this.localDb.getAgents(); } } async updateAgent(id, updates) { this.ensureInitialized(); const updatedAgent = { ...updates, id, lastActiveAt: new Date() }; if (this.useCBD) { return await this.adapter.updateAgent(id, updates); } else { const existing = await this.localDb.getAgent(id); if (!existing) { throw new Error(`Agent ${id} not found`); } const merged = { ...existing, ...updatedAgent }; return this.localDb.updateAgent(merged); } } async deleteAgent(id) { this.ensureInitialized(); if (this.useCBD) { return await this.adapter.deleteAgent(id); } else { return await this.localDb.deleteAgent(id); } } // ======================================== // WORKSPACE AND ANALYTICS METHODS WITH FALLBACK // ======================================== async getWorkspaceMetrics(workspaceId) { this.ensureInitialized(); if (this.useCBD) { return await this.adapter.getWorkspaceMetrics(workspaceId); } else { const projects = await this.localDb.getProjects(); const tasks = this.localDb.getTasks(); const agents = await this.localDb.getAgents(); return { workspaceId, metrics: { totalProjects: projects.length, activeProjects: projects.filter(p => p.status === 'active').length, completedProjects: projects.filter(p => p.status === 'completed').length, totalTasks: tasks.length, completedTasks: tasks.filter(t => t.status === 'completed').length, totalAgents: agents.length, activeAgents: agents.filter(a => a.status === 'available').length } }; } } async searchEntities(query, entityType, limit = 100) { this.ensureInitialized(); if (this.useCBD) { return await this.adapter.searchEntities(query, entityType, limit); } else { const results = []; const queryLower = query.toLowerCase(); if (!entityType || entityType === 'project') { const projects = await this.localDb.getProjects(); results.push(...projects.filter(p => p.name.toLowerCase().includes(queryLower) || p.description.toLowerCase().includes(queryLower)).slice(0, limit)); } if (!entityType || entityType === 'task') { const tasks = this.localDb.getTasks(); results.push(...tasks.filter(t => t.title.toLowerCase().includes(queryLower) || t.description.toLowerCase().includes(queryLower)).slice(0, limit)); } if (!entityType || entityType === 'agent') { const agents = await this.localDb.getAgents(); results.push(...agents.filter(a => a.name.toLowerCase().includes(queryLower)).slice(0, limit)); } return results.slice(0, limit); } } // ======================================== // UTILITY METHODS // ======================================== async healthCheck() { if (!this.isInitialized) { return { status: 'not_initialized', version: '2.1.0', timestamp: new Date().toISOString(), mode: 'unknown' }; } if (this.useCBD) { try { const health = await this.adapter.healthCheck(); return { ...health, mode: 'CBD Service' }; } catch (error) { return { status: 'error', version: '2.1.0', timestamp: new Date().toISOString(), mode: 'CBD Service (error)' }; } } else { return { status: 'healthy', version: '2.1.0', timestamp: new Date().toISOString(), mode: 'Local In-Memory (Fallback)' }; } } ensureInitialized() { if (!this.isInitialized) { throw new Error('DatabaseService not initialized. Call initialize() first.'); } } getDatabaseMode() { this.ensureInitialized(); return this.useCBD ? 'CBD Service' : 'Local In-Memory (Fallback)'; } isUsingCBD() { return this.useCBD; } async close() { console.log('[ControlAI] Database service closing...'); await this.cleanup(); this.isInitialized = false; } }