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.
709 lines (629 loc) • 19.7 kB
JavaScript
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const yaml = require('js-yaml');
/**
* Generador de contratos Frontend-Backend
* Analiza la configuración del backend MCP y genera un contrato JSON
* que el frontend puede usar para auto-generarse
*/
class ContractGenerator {
constructor(projectRoot = process.cwd()) {
this.projectRoot = projectRoot;
this.modulesPath = path.join(projectRoot, 'modules');
this.packageJson = this.loadPackageJson();
this.contract = {
contract: {
version: this.packageJson.version || '1.0.0',
type: 'backend-frontend-bridge',
generatedAt: new Date().toISOString(),
generatedBy: `backend-mcp-v${this.packageJson.version || '1.0.0'}`,
compatibility: {
minFrontendMcpVersion: '1.0.0',
backendMcpVersion: this.packageJson.version || '1.0.0',
contractHash: '',
checksums: {}
}
}
};
}
/**
* Carga el package.json del proyecto
*/
loadPackageJson() {
try {
const packagePath = path.join(this.projectRoot, 'package.json');
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
} catch (error) {
console.warn('No se pudo cargar package.json:', error.message);
return {};
}
}
/**
* Genera el contrato completo
*/
async generateContract() {
console.log('🚀 Generando contrato frontend-backend...');
// Analizar información del proyecto
this.contract.project = this.analyzeProject();
// Analizar configuración de API
this.contract.api = this.analyzeApiConfig();
// Analizar módulos habilitados
this.contract.modules = await this.analyzeModules();
// Analizar entidades CRUD
this.contract.entities = await this.analyzeEntities();
// Analizar configuración de autenticación
this.contract.auth = this.analyzeAuth();
// Analizar eventos
this.contract.events = this.analyzeEvents();
// Analizar validaciones
this.contract.validation = this.analyzeValidation();
// Configuración para frontend
this.contract.frontend = this.analyzeFrontendConfig();
// Información de deployment
this.contract.deployment = this.analyzeDeployment();
// Información de testing
this.contract.testing = this.analyzeTesting();
// Configuración MCP
this.contract.mcp = this.analyzeMcpConfig();
// Generar checksums
this.generateChecksums();
return this.contract;
}
/**
* Analiza información básica del proyecto
*/
analyzeProject() {
const envExample = this.loadEnvExample();
return {
name: this.packageJson.name || 'backend-project',
domain: this.packageJson.description || 'Backend API Project',
description: this.packageJson.description || 'API backend generada con MCP',
mode: envExample.NODE_ENV === 'production' ? 'production' : 'development',
architecture: 'monolithic',
database: this.detectDatabaseType(envExample),
environment: envExample.NODE_ENV || 'development'
};
}
/**
* Analiza configuración de API
*/
analyzeApiConfig() {
const envExample = this.loadEnvExample();
const port = envExample.PORT || 3000;
const baseUrl = envExample.BASE_URL || `http://localhost:${port}`;
return {
baseUrl: baseUrl,
version: 'v1',
prefix: '/api/v1',
documentation: {
openapi: '/api/v1/docs/openapi.json',
swagger: '/api/v1/docs',
postman: '/api/v1/docs/postman.json'
},
rateLimit: {
enabled: true,
requests: 1000,
window: '15m',
skipSuccessfulRequests: false
},
cors: {
enabled: true,
origins: [baseUrl.replace(/:\d+$/, ':3000')],
credentials: true
},
middleware: this.detectMiddleware()
};
}
/**
* Analiza módulos habilitados
*/
async analyzeModules() {
const modules = {};
if (!fs.existsSync(this.modulesPath)) {
return modules;
}
const moduleDirectories = fs.readdirSync(this.modulesPath)
.filter(dir => fs.statSync(path.join(this.modulesPath, dir)).isDirectory());
for (const moduleDir of moduleDirectories) {
const modulePath = path.join(this.modulesPath, moduleDir);
const manifestPath = path.join(modulePath, 'manifest.yaml');
if (fs.existsSync(manifestPath)) {
try {
const manifest = yaml.load(fs.readFileSync(manifestPath, 'utf8'));
modules[moduleDir] = {
enabled: true,
version: manifest.version || '1.0.0',
...manifest,
config: await this.analyzeModuleConfig(moduleDir, modulePath)
};
} catch (error) {
console.warn(`Error cargando módulo ${moduleDir}:`, error.message);
}
}
}
return modules;
}
/**
* Analiza configuración específica de un módulo
*/
async analyzeModuleConfig(moduleName, modulePath) {
const config = {};
// Leer configuración desde init.js si existe
const initPath = path.join(modulePath, 'init.js');
if (fs.existsSync(initPath)) {
try {
const initContent = fs.readFileSync(initPath, 'utf8');
// Extraer configuraciones comunes
if (moduleName === 'auth') {
config.strategy = initContent.includes('jwt') ? 'jwt' : 'session';
config.features = this.extractAuthFeatures(initContent);
} else if (moduleName === 'database') {
config.provider = this.extractDatabaseProvider(initContent);
config.features = this.extractDatabaseFeatures(initContent);
} else if (moduleName === 'email') {
config.provider = this.extractEmailProvider(initContent);
config.features = this.extractEmailFeatures(initContent);
}
} catch (error) {
console.warn(`Error analizando configuración de ${moduleName}:`, error.message);
}
}
return config;
}
/**
* Analiza entidades CRUD generadas
*/
async analyzeEntities() {
const entities = [];
const crudPath = path.join(this.modulesPath, 'crud');
if (!fs.existsSync(crudPath)) {
return entities;
}
// Buscar proyectos generados que contengan entidades
const srcPath = path.join(this.projectRoot, 'src');
if (fs.existsSync(srcPath)) {
const entityDirs = fs.readdirSync(srcPath)
.filter(dir => {
const dirPath = path.join(srcPath, dir);
return fs.statSync(dirPath).isDirectory() &&
fs.existsSync(path.join(dirPath, 'controller.ts')) &&
fs.existsSync(path.join(dirPath, 'service.ts'));
});
for (const entityDir of entityDirs) {
const entity = await this.analyzeEntity(entityDir, path.join(srcPath, entityDir));
if (entity) {
entities.push(entity);
}
}
}
return entities;
}
/**
* Analiza una entidad específica
*/
async analyzeEntity(entityName, entityPath) {
try {
const entity = {
name: this.capitalize(entityName),
table: entityName.toLowerCase() + 's',
primaryKey: 'id',
timestamps: true,
softDeletes: true,
audit: false,
apiPath: `/${entityName.toLowerCase()}s`,
fields: [],
relations: [],
endpoints: {},
ui: {}
};
// Analizar modelo/DTO para extraer campos
const dtoPath = path.join(entityPath, 'dto.ts');
if (fs.existsSync(dtoPath)) {
entity.fields = this.extractFieldsFromDto(dtoPath);
}
// Analizar rutas para extraer endpoints
const routesPath = path.join(entityPath, 'routes.ts');
if (fs.existsSync(routesPath)) {
entity.endpoints = this.extractEndpointsFromRoutes(routesPath, entityName);
}
// Analizar validaciones
const validatorPath = path.join(entityPath, 'validator.ts');
if (fs.existsSync(validatorPath)) {
entity.validation = this.extractValidationFromValidator(validatorPath);
}
// Configuración de UI básica
entity.ui = this.generateBasicUiConfig(entity.fields);
return entity;
} catch (error) {
console.warn(`Error analizando entidad ${entityName}:`, error.message);
return null;
}
}
/**
* Extrae campos desde el archivo DTO
*/
extractFieldsFromDto(dtoPath) {
const fields = [
{
name: 'id',
type: 'uuid',
primaryKey: true,
generated: 'uuid'
},
{
name: 'name',
type: 'string',
required: true,
validation: 'min:2,max:100'
},
{
name: 'description',
type: 'text',
nullable: true
},
{
name: 'status',
type: 'enum',
enum: ['active', 'inactive', 'pending'],
default: 'active'
},
{
name: 'createdAt',
type: 'timestamp',
generated: 'timestamp'
},
{
name: 'updatedAt',
type: 'timestamp',
generated: 'timestamp'
}
];
// TODO: Implementar parsing real del archivo DTO
// Por ahora retornamos campos básicos comunes
return fields;
}
/**
* Extrae endpoints desde el archivo de rutas
*/
extractEndpointsFromRoutes(routesPath, entityName) {
const entityLower = entityName.toLowerCase();
const entityPlural = entityLower + 's';
return {
list: {
method: 'GET',
path: `/${entityPlural}`,
permissions: ['admin', 'manager', 'employee'],
pagination: true,
filters: ['status'],
search: ['name']
},
show: {
method: 'GET',
path: `/${entityPlural}/:id`,
permissions: ['admin', 'manager', 'employee']
},
create: {
method: 'POST',
path: `/${entityPlural}`,
permissions: ['admin', 'manager'],
validation: `Create${this.capitalize(entityName)}Request`
},
update: {
method: 'PUT',
path: `/${entityPlural}/:id`,
permissions: ['admin', 'manager'],
validation: `Update${this.capitalize(entityName)}Request`
},
delete: {
method: 'DELETE',
path: `/${entityPlural}/:id`,
permissions: ['admin'],
softDelete: true
}
};
}
/**
* Genera configuración básica de UI
*/
generateBasicUiConfig(fields) {
const displayFields = fields
.filter(f => !['createdAt', 'updatedAt'].includes(f.name))
.map(f => f.name);
return {
listView: {
columns: displayFields.slice(0, 5),
searchable: ['name'],
filterable: ['status'],
sortable: ['name', 'createdAt'],
defaultSort: 'createdAt:desc'
},
formView: {
layout: 'sections',
sections: [
{
title: 'Información General',
fields: displayFields
}
]
}
};
}
/**
* Analiza configuración de autenticación
*/
analyzeAuth() {
return {
roles: [
{
name: 'admin',
description: 'Administrador del sistema',
permissions: ['*']
},
{
name: 'manager',
description: 'Gerente',
permissions: ['read', 'create', 'update']
},
{
name: 'employee',
description: 'Empleado',
permissions: ['read']
}
],
middleware: {
authenticate: 'verifyJwtToken',
authorize: 'checkPermissions',
rateLimit: 'authRateLimit'
}
};
}
/**
* Analiza eventos del sistema
*/
analyzeEvents() {
return {
enabled: true,
handlers: [
{
event: 'entity.created',
handlers: ['SendNotification', 'UpdateAnalytics']
},
{
event: 'user.registered',
handlers: ['SendWelcomeEmail', 'CreateUserProfile']
}
]
};
}
/**
* Analiza validaciones
*/
analyzeValidation() {
return {
requests: {
CreateEntityRequest: {
name: 'required|string|min:2|max:100',
description: 'string|max:500',
status: 'string|in:active,inactive,pending'
},
UpdateEntityRequest: {
name: 'string|min:2|max:100',
description: 'string|max:500',
status: 'string|in:active,inactive,pending'
}
}
};
}
/**
* Analiza configuración para frontend
*/
analyzeFrontendConfig() {
return {
compatibility: {
frameworks: ['react', 'vue', 'angular'],
recommended: 'react'
},
autoGenerate: {
pages: true,
components: true,
routing: true,
forms: true,
validation: true,
permissions: true
},
ui: {
theme: {
primaryColor: '#3b82f6',
secondaryColor: '#64748b',
darkMode: true
},
layout: 'sidebar',
features: [
'responsive',
'dark-mode',
'accessibility',
'real-time-updates'
]
},
sync: {
realTime: false,
websocket: null,
events: []
}
};
}
/**
* Analiza configuración de deployment
*/
analyzeDeployment() {
const envExample = this.loadEnvExample();
return {
environment: envExample.NODE_ENV || 'development',
infrastructure: {
platform: 'docker',
database: {
provider: this.detectDatabaseType(envExample),
ssl: envExample.DATABASE_SSL === 'true'
}
}
};
}
/**
* Analiza configuración de testing
*/
analyzeTesting() {
const hasJest = this.packageJson.devDependencies?.jest ||
this.packageJson.dependencies?.jest ||
fs.existsSync(path.join(this.projectRoot, 'jest.config.js'));
return {
coverage: {
minimum: 80,
current: 0
},
types: ['unit', 'integration'],
frameworks: hasJest ? ['jest'] : []
};
}
/**
* Analiza configuración MCP
*/
analyzeMcpConfig() {
return {
generation: {
strategy: 'modular',
codeStyle: 'clean-architecture',
patterns: ['repository', 'service', 'controller'],
features: ['validation', 'error-handling', 'logging']
},
frontend: {
framework: 'react',
stateManagement: 'zustand',
styling: 'tailwindcss',
routing: 'react-router',
forms: 'react-hook-form',
validation: 'joi'
},
sync: {
autoUpdate: true,
versionControl: 'git',
deployOnSync: false
}
};
}
/**
* Genera checksums para integridad
*/
generateChecksums() {
const contractString = JSON.stringify(this.contract, null, 2);
const hash = crypto.createHash('sha256').update(contractString).digest('hex');
this.contract.contract.compatibility.contractHash = `sha256:${hash.substring(0, 16)}...`;
this.contract.contract.compatibility.checksums = {
entities: `sha256:${crypto.createHash('sha256').update(JSON.stringify(this.contract.entities)).digest('hex').substring(0, 16)}...`,
auth: `sha256:${crypto.createHash('sha256').update(JSON.stringify(this.contract.auth)).digest('hex').substring(0, 16)}...`,
modules: `sha256:${crypto.createHash('sha256').update(JSON.stringify(this.contract.modules)).digest('hex').substring(0, 16)}...`
};
}
// Métodos auxiliares
loadEnvExample() {
try {
const envPath = path.join(this.projectRoot, '.env.example');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf8');
const env = {};
envContent.split('\n').forEach(line => {
const [key, value] = line.split('=');
if (key && value) {
env[key.trim()] = value.trim().replace(/["']/g, '');
}
});
return env;
}
} catch (error) {
console.warn('Error cargando .env.example:', error.message);
}
return {};
}
detectDatabaseType(env) {
const dbUrl = env.DATABASE_URL || '';
if (dbUrl.includes('postgresql') || dbUrl.includes('postgres')) return 'postgresql';
if (dbUrl.includes('mysql')) return 'mysql';
if (dbUrl.includes('sqlite')) return 'sqlite';
if (dbUrl.includes('mongodb')) return 'mongodb';
return 'postgresql'; // default
}
detectMiddleware() {
const middleware = ['helmet', 'compression', 'morgan', 'cors'];
// Verificar si existen en package.json
const deps = { ...this.packageJson.dependencies, ...this.packageJson.devDependencies };
if (deps['express-rate-limit']) middleware.push('express-rate-limit');
if (deps['express-validator']) middleware.push('express-validator');
return middleware;
}
extractAuthFeatures(content) {
const features = [];
if (content.includes('refresh')) features.push('refresh-tokens');
if (content.includes('email') && content.includes('verify')) features.push('email-verification');
if (content.includes('password') && content.includes('reset')) features.push('password-reset');
return features;
}
extractDatabaseProvider(content) {
if (content.includes('postgresql') || content.includes('postgres')) return 'postgresql';
if (content.includes('mysql')) return 'mysql';
if (content.includes('sqlite')) return 'sqlite';
if (content.includes('mongodb')) return 'mongodb';
return 'postgresql';
}
extractDatabaseFeatures(content) {
const features = [];
if (content.includes('migration')) features.push('migrations');
if (content.includes('seed')) features.push('seeders');
if (content.includes('soft') && content.includes('delete')) features.push('soft-deletes');
if (content.includes('audit')) features.push('audit-trail');
return features;
}
extractEmailProvider(content) {
if (content.includes('resend')) return 'resend';
if (content.includes('sendgrid')) return 'sendgrid';
if (content.includes('mailgun')) return 'mailgun';
if (content.includes('ses')) return 'ses';
return 'smtp';
}
extractEmailFeatures(content) {
const features = [];
if (content.includes('template')) features.push('templates');
if (content.includes('queue')) features.push('queue');
if (content.includes('track')) features.push('tracking');
return features;
}
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Guarda el contrato en un archivo
*/
async saveContract(outputPath = null) {
const contract = await this.generateContract();
const filePath = outputPath || path.join(this.projectRoot, 'frontend-contract.json');
fs.writeFileSync(filePath, JSON.stringify(contract, null, 2), 'utf8');
console.log(`✅ Contrato generado exitosamente: ${filePath}`);
return filePath;
}
}
// Ejecutar si se llama directamente
if (require.main === module) {
const generator = new ContractGenerator();
const outputPath = process.argv[2];
generator.saveContract(outputPath)
.then(filePath => {
console.log('🎉 Generación completada!');
console.log(`📄 Archivo: ${filePath}`);
console.log('\n💡 Usa este contrato para generar automáticamente el frontend.');
})
.catch(error => {
console.error('❌ Error generando contrato:', error);
process.exit(1);
});
}
module.exports = ContractGenerator;