backend-mcp
Version:
Generador automático de backends con Node.js, Express, Prisma y módulos configurables. Servidor MCP compatible con npx para agentes IA. Soporta PostgreSQL, MySQL, MongoDB y SQLite.
407 lines (328 loc) • 12.4 kB
JavaScript
/**
* 🔍 Script de Validación de Módulos MCP Backend
*
* Este script valida la integridad y estructura de todos los módulos
* del framework MCP Backend.
*/
const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
const chalk = require('chalk');
const glob = require('glob');
class ModuleValidator {
constructor() {
this.modulesPath = path.join(__dirname, '..', 'modules');
this.errors = [];
this.warnings = [];
this.validatedModules = [];
}
async validate() {
console.log(chalk.blue.bold('\n🔍 Validador de Módulos MCP Backend\n'));
try {
const modules = await this.getModules();
if (modules.length === 0) {
this.addError('No se encontraron módulos en la carpeta modules/');
return this.showResults();
}
console.log(chalk.gray(`Validando ${modules.length} módulos...\n`));
for (const module of modules) {
await this.validateModule(module);
}
this.showResults();
} catch (error) {
console.error(chalk.red('❌ Error durante la validación:'), error.message);
process.exit(1);
}
}
async getModules() {
const moduleDirs = await fs.readdir(this.modulesPath);
const modules = [];
for (const dir of moduleDirs) {
const modulePath = path.join(this.modulesPath, dir);
const stat = await fs.stat(modulePath);
if (stat.isDirectory()) {
modules.push({
name: dir,
path: modulePath
});
}
}
return modules;
}
async validateModule(module) {
console.log(chalk.yellow(`🔍 Validando módulo: ${module.name}`));
const validationResults = {
name: module.name,
path: module.path,
hasManifest: false,
hasInit: false,
hasReadme: false,
hasTemplates: false,
manifestValid: false,
errors: [],
warnings: []
};
// Validar estructura básica
await this.validateModuleStructure(module, validationResults);
// Validar manifest.yaml
await this.validateManifest(module, validationResults);
// Validar init.js
await this.validateInitScript(module, validationResults);
// Validar README.md
await this.validateReadme(module, validationResults);
// Validar templates
await this.validateTemplates(module, validationResults);
this.validatedModules.push(validationResults);
// Mostrar resultado del módulo
this.showModuleResult(validationResults);
}
async validateModuleStructure(module, results) {
const requiredFiles = [
'manifest.yaml',
'init.js',
'README.md'
];
const optionalDirs = [
'templates',
'examples',
'src',
'docs'
];
// Verificar archivos requeridos
for (const file of requiredFiles) {
const filePath = path.join(module.path, file);
const exists = await fs.pathExists(filePath);
switch (file) {
case 'manifest.yaml':
results.hasManifest = exists;
break;
case 'init.js':
results.hasInit = exists;
break;
case 'README.md':
results.hasReadme = exists;
break;
}
if (!exists) {
results.errors.push(`Archivo requerido faltante: ${file}`);
}
}
// Verificar directorios opcionales
for (const dir of optionalDirs) {
const dirPath = path.join(module.path, dir);
const exists = await fs.pathExists(dirPath);
if (dir === 'templates') {
results.hasTemplates = exists;
}
if (!exists && dir === 'templates') {
results.warnings.push(`Directorio recomendado faltante: ${dir}`);
}
}
}
async validateManifest(module, results) {
if (!results.hasManifest) return;
try {
const manifestPath = path.join(module.path, 'manifest.yaml');
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const manifest = yaml.load(manifestContent);
// Validar estructura del manifest
const requiredFields = [
'module.name',
'module.version',
'module.description',
'module.category'
];
for (const field of requiredFields) {
if (!this.getNestedProperty(manifest, field)) {
results.errors.push(`Campo requerido faltante en manifest: ${field}`);
}
}
// Validar que el nombre coincida con el directorio
if (manifest.module && manifest.module.name !== module.name) {
results.errors.push(`El nombre en manifest (${manifest.module.name}) no coincide con el directorio (${module.name})`);
}
// Validar versión semántica
if (manifest.module && manifest.module.version) {
const versionRegex = /^\d+\.\d+\.\d+$/;
if (!versionRegex.test(manifest.module.version)) {
results.warnings.push('La versión no sigue el formato semántico (x.y.z)');
}
}
// Validar dependencias
if (manifest.dependencies) {
if (manifest.dependencies.required && !Array.isArray(manifest.dependencies.required)) {
results.errors.push('dependencies.required debe ser un array');
}
if (manifest.dependencies.optional && !Array.isArray(manifest.dependencies.optional)) {
results.errors.push('dependencies.optional debe ser un array');
}
}
// Validar triggers
if (manifest.triggers && !Array.isArray(manifest.triggers)) {
results.errors.push('triggers debe ser un array');
}
results.manifestValid = results.errors.length === 0;
} catch (error) {
results.errors.push(`Error al parsear manifest.yaml: ${error.message}`);
}
}
async validateInitScript(module, results) {
if (!results.hasInit) return;
try {
const initPath = path.join(module.path, 'init.js');
const initContent = await fs.readFile(initPath, 'utf8');
// Verificar que sea un módulo válido de Node.js
if (!initContent.includes('module.exports') && !initContent.includes('exports.')) {
results.warnings.push('init.js no parece exportar una función');
}
// Verificar que tenga una función async
if (!initContent.includes('async') && !initContent.includes('Promise')) {
results.warnings.push('init.js debería ser asíncrono para mejor compatibilidad');
}
// Verificar manejo de errores básico
if (!initContent.includes('try') && !initContent.includes('catch')) {
results.warnings.push('init.js debería incluir manejo de errores');
}
} catch (error) {
results.errors.push(`Error al leer init.js: ${error.message}`);
}
}
async validateReadme(module, results) {
if (!results.hasReadme) return;
try {
const readmePath = path.join(module.path, 'README.md');
const readmeContent = await fs.readFile(readmePath, 'utf8');
// Verificar secciones básicas
const requiredSections = [
'# ', // Título
'## ', // Secciones
'Características',
'Configuración',
'Uso'
];
for (const section of requiredSections) {
if (!readmeContent.includes(section)) {
results.warnings.push(`Sección recomendada faltante en README: ${section}`);
}
}
// Verificar longitud mínima
if (readmeContent.length < 500) {
results.warnings.push('README.md parece muy corto (< 500 caracteres)');
}
// Verificar ejemplos de código
if (!readmeContent.includes('```')) {
results.warnings.push('README.md debería incluir ejemplos de código');
}
} catch (error) {
results.errors.push(`Error al leer README.md: ${error.message}`);
}
}
async validateTemplates(module, results) {
if (!results.hasTemplates) return;
try {
const templatesPath = path.join(module.path, 'templates');
const templateFiles = await glob('**/*.hbs', { cwd: templatesPath });
if (templateFiles.length === 0) {
results.warnings.push('Directorio templates existe pero no contiene archivos .hbs');
}
// Validar cada template
for (const templateFile of templateFiles) {
const templatePath = path.join(templatesPath, templateFile);
const templateContent = await fs.readFile(templatePath, 'utf8');
// Verificar sintaxis básica de Handlebars
const handlebarsRegex = /{{.*?}}/g;
const matches = templateContent.match(handlebarsRegex);
if (!matches || matches.length === 0) {
results.warnings.push(`Template ${templateFile} no contiene variables de Handlebars`);
}
}
} catch (error) {
results.warnings.push(`Error al validar templates: ${error.message}`);
}
}
getNestedProperty(obj, path) {
return path.split('.').reduce((current, key) => current && current[key], obj);
}
showModuleResult(results) {
const hasErrors = results.errors.length > 0;
const hasWarnings = results.warnings.length > 0;
if (hasErrors) {
console.log(chalk.red(` ❌ ${results.name} - ${results.errors.length} errores`));
this.errors.push(...results.errors.map(err => `[${results.name}] ${err}`));
} else if (hasWarnings) {
console.log(chalk.yellow(` ⚠️ ${results.name} - ${results.warnings.length} advertencias`));
} else {
console.log(chalk.green(` ✅ ${results.name} - Válido`));
}
this.warnings.push(...results.warnings.map(warn => `[${results.name}] ${warn}`));
}
showResults() {
console.log(chalk.blue.bold('\n📊 Resultados de Validación\n'));
const totalModules = this.validatedModules.length;
const validModules = this.validatedModules.filter(m => m.errors.length === 0).length;
const modulesWithWarnings = this.validatedModules.filter(m => m.warnings.length > 0).length;
console.log(chalk.gray(`Total de módulos: ${totalModules}`));
console.log(chalk.green(`Módulos válidos: ${validModules}`));
console.log(chalk.yellow(`Módulos con advertencias: ${modulesWithWarnings}`));
console.log(chalk.red(`Módulos con errores: ${totalModules - validModules}\n`));
// Mostrar errores
if (this.errors.length > 0) {
console.log(chalk.red.bold('❌ Errores encontrados:'));
this.errors.forEach(error => {
console.log(chalk.red(` • ${error}`));
});
console.log('');
}
// Mostrar advertencias
if (this.warnings.length > 0) {
console.log(chalk.yellow.bold('⚠️ Advertencias:'));
this.warnings.forEach(warning => {
console.log(chalk.yellow(` • ${warning}`));
});
console.log('');
}
// Resumen final
if (this.errors.length === 0) {
console.log(chalk.green.bold('🎉 Todos los módulos son válidos!'));
if (this.warnings.length > 0) {
console.log(chalk.yellow('💡 Considera revisar las advertencias para mejorar la calidad.'));
}
} else {
console.log(chalk.red.bold('🚨 Se encontraron errores que deben ser corregidos.'));
process.exit(1);
}
// Generar reporte detallado
this.generateReport();
}
async generateReport() {
const report = {
timestamp: new Date().toISOString(),
summary: {
totalModules: this.validatedModules.length,
validModules: this.validatedModules.filter(m => m.errors.length === 0).length,
modulesWithWarnings: this.validatedModules.filter(m => m.warnings.length > 0).length,
totalErrors: this.errors.length,
totalWarnings: this.warnings.length
},
modules: this.validatedModules,
errors: this.errors,
warnings: this.warnings
};
const reportPath = path.join(__dirname, '..', 'validation-report.json');
await fs.writeJson(reportPath, report, { spaces: 2 });
console.log(chalk.gray(`\n📄 Reporte detallado guardado en: ${reportPath}`));
}
addError(message) {
this.errors.push(message);
}
addWarning(message) {
this.warnings.push(message);
}
}
// Ejecutar si es llamado directamente
if (require.main === module) {
const validator = new ModuleValidator();
validator.validate().catch(console.error);
}
module.exports = ModuleValidator;