#!/usr/bin/env node
/**
* MCP Server para RifaExpress Backend
*
* Este servidor expone tools para interactuar con el backend de RifaExpress:
* - Queries a PostgreSQL
* - Inspección de schema
* - Consultas a entidades (Company, User, Plan, Subscription)
*
* Variables de entorno requeridas:
* - DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { healthCheck, closePool } from './db.js'
import { config } from './config.js'
import * as tools from './tools/index.js'
// ============================================
// SERVER SETUP
// ============================================
const server = new Server(
{
name: 'mcp-rifaexpress',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
)
// ============================================
// TOOL DEFINITIONS
// ============================================
const toolDefinitions = [
// === DATABASE TOOLS ===
{
name: 'rifaexpress_query_db',
description:
'Ejecuta una query SELECT en la base de datos PostgreSQL de RifaExpress. Solo queries SELECT están permitidas por seguridad.',
inputSchema: {
type: 'object',
properties: {
sql: {
type: 'string',
description: 'SQL SELECT query to execute',
},
limit: {
type: 'number',
description: 'Limit results (default: 100)',
},
},
required: ['sql'],
},
},
{
name: 'rifaexpress_inspect_table',
description:
'Inspecciona el schema de una tabla específica (columnas, tipos, nullability, etc.).',
inputSchema: {
type: 'object',
properties: {
tableName: {
type: 'string',
description: 'Name of the table to inspect',
},
},
required: ['tableName'],
},
},
{
name: 'rifaexpress_list_tables',
description: 'Lista todas las tablas en el schema especificado.',
inputSchema: {
type: 'object',
properties: {
schema: {
type: 'string',
description: 'Schema name (default: public)',
},
},
},
},
// === ENTITY TOOLS ===
{
name: 'rifaexpress_get_companies',
description:
'Obtiene companies (empresas) con filtros opcionales de subscriptionStatus.',
inputSchema: {
type: 'object',
properties: {
subscriptionStatus: {
type: 'string',
enum: ['active', 'inactive', 'past_due'],
description: 'Filter by subscription status',
},
limit: {
type: 'number',
description: 'Limit results (default: 50)',
},
},
},
},
{
name: 'rifaexpress_get_users',
description: 'Obtiene users con filtros opcionales de companyId y role.',
inputSchema: {
type: 'object',
properties: {
companyId: {
type: 'string',
description: 'Filter by company ID',
},
role: {
type: 'number',
description: 'RoleEnum value (3=ASSISTANT, 7=AGENT, 15=ADMIN)',
},
limit: {
type: 'number',
description: 'Limit results (default: 50)',
},
},
},
},
{
name: 'rifaexpress_get_plans',
description: 'Obtiene plans de suscripción.',
inputSchema: {
type: 'object',
properties: {
isActive: {
type: 'boolean',
description: 'Filter by active status',
},
},
},
},
{
name: 'rifaexpress_get_subscriptions',
description:
'Obtiene subscriptions (si la tabla existe). Esta entidad se creará en Fase 1 del SaaS Core.',
inputSchema: {
type: 'object',
properties: {
companyId: {
type: 'string',
description: 'Filter by company ID',
},
status: {
type: 'string',
enum: ['active', 'past_due', 'canceled', 'incomplete'],
description: 'Filter by subscription status',
},
limit: {
type: 'number',
description: 'Limit results (default: 50)',
},
},
},
},
// === UTILITIES ===
{
name: 'rifaexpress_health_check',
description:
'Verifica la conexión con la base de datos PostgreSQL. Útil para diagnosticar problemas de configuración.',
inputSchema: {
type: 'object',
properties: {},
},
},
]
// ============================================
// HANDLERS
// ============================================
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: toolDefinitions,
}
})
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
try {
let result: string
switch (name) {
// Database Tools
case 'rifaexpress_query_db':
result = await tools.queryDb(tools.queryDbSchema.parse(args))
break
case 'rifaexpress_inspect_table':
result = await tools.inspectTable(tools.inspectTableSchema.parse(args))
break
case 'rifaexpress_list_tables':
result = await tools.listTables(tools.listTablesSchema.parse(args))
break
// Entity Tools
case 'rifaexpress_get_companies':
result = await tools.getCompanies(tools.getCompaniesSchema.parse(args))
break
case 'rifaexpress_get_users':
result = await tools.getUsers(tools.getUsersSchema.parse(args))
break
case 'rifaexpress_get_plans':
result = await tools.getPlans(tools.getPlansSchema.parse(args))
break
case 'rifaexpress_get_subscriptions':
result = await tools.getSubscriptions(
tools.getSubscriptionsSchema.parse(args)
)
break
// Utilities
case 'rifaexpress_health_check':
const health = await healthCheck()
result = JSON.stringify(
{
...health,
config: {
dbHost: config.db.host,
dbPort: config.db.port,
dbName: config.db.database,
},
},
null,
2
)
break
default:
throw new Error(`Tool desconocido: ${name}`)
}
return {
content: [
{
type: 'text',
text: result,
},
],
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Error desconocido'
return {
content: [
{
type: 'text',
text: JSON.stringify({ error: true, message }, null, 2),
},
],
isError: true,
}
}
})
// ============================================
// START SERVER
// ============================================
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
// Log to stderr to not interfere with MCP protocol
console.error('MCP RifaExpress Backend Server iniciado')
console.error(`DB: ${config.db.user}@${config.db.host}:${config.db.port}/${config.db.database}`)
}
// Cleanup on exit
process.on('SIGINT', async () => {
await closePool()
process.exit(0)
})
process.on('SIGTERM', async () => {
await closePool()
process.exit(0)
})
main().catch((error) => {
console.error('Error fatal:', error)
process.exit(1)
})