@fullqueso/mcp-bc-gastos 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import { existsSync, readFileSync } from 'fs';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ // Only load .env if running from source (local development).
11
+ // When installed via npm/npx, env vars come from the MCP client config.
12
+ const envPath = join(__dirname, '.env');
13
+ if (existsSync(envPath)) {
14
+ const dotenv = await import('dotenv');
15
+ dotenv.config({ path: envPath });
16
+ }
17
+
18
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
19
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
+ import {
21
+ CallToolRequestSchema,
22
+ ListToolsRequestSchema,
23
+ } from '@modelcontextprotocol/sdk/types.js';
24
+
25
+ import { BCClient } from './lib/bc-client.js';
26
+ import { expenseAnalysisTool, handleExpenseAnalysis } from './tools/expense-analysis.js';
27
+ import { efficiencyRatiosTool, handleEfficiencyRatios } from './tools/efficiency-ratios.js';
28
+ import { storeComparisonTool, handleStoreComparison } from './tools/store-comparison.js';
29
+ import { anomalyDetectionTool, handleAnomalyDetection } from './tools/anomaly-detection.js';
30
+ import { trendsTool, handleTrends } from './tools/trends.js';
31
+ import { expenseDetailsTool, handleExpenseDetails } from './tools/expense-details.js';
32
+ import { accountTransactionsTool, handleAccountTransactions } from './tools/account-transactions.js';
33
+
34
+ const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
35
+ const bcClient = new BCClient();
36
+
37
+ const server = new Server(
38
+ {
39
+ name: pkg.name,
40
+ version: pkg.version,
41
+ },
42
+ {
43
+ capabilities: {
44
+ tools: {},
45
+ },
46
+ }
47
+ );
48
+
49
+ // List all available tools
50
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
51
+ return {
52
+ tools: [
53
+ expenseAnalysisTool,
54
+ efficiencyRatiosTool,
55
+ storeComparisonTool,
56
+ anomalyDetectionTool,
57
+ trendsTool,
58
+ expenseDetailsTool,
59
+ accountTransactionsTool,
60
+ ],
61
+ };
62
+ });
63
+
64
+ // Handle tool calls
65
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
66
+ const { name, arguments: args } = request.params;
67
+
68
+ try {
69
+ let result;
70
+
71
+ switch (name) {
72
+ case 'get_expense_analysis':
73
+ result = await handleExpenseAnalysis(bcClient, args);
74
+ break;
75
+ case 'get_efficiency_ratios':
76
+ result = await handleEfficiencyRatios(bcClient, args);
77
+ break;
78
+ case 'compare_stores':
79
+ result = await handleStoreComparison(bcClient, args);
80
+ break;
81
+ case 'detect_anomalies':
82
+ result = await handleAnomalyDetection(bcClient, args);
83
+ break;
84
+ case 'get_trends':
85
+ result = await handleTrends(bcClient, args);
86
+ break;
87
+ case 'get_expense_details':
88
+ result = await handleExpenseDetails(bcClient, args);
89
+ break;
90
+ case 'get_account_transactions':
91
+ result = await handleAccountTransactions(bcClient, args);
92
+ break;
93
+ default:
94
+ return {
95
+ content: [{ type: 'text', text: `Herramienta desconocida: ${name}` }],
96
+ isError: true,
97
+ };
98
+ }
99
+
100
+ return {
101
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
102
+ };
103
+ } catch (error) {
104
+ console.error(`Error ejecutando herramienta ${name}:`, error);
105
+ return {
106
+ content: [
107
+ {
108
+ type: 'text',
109
+ text: `Error: ${error.message}`,
110
+ },
111
+ ],
112
+ isError: true,
113
+ };
114
+ }
115
+ });
116
+
117
+ function validateEnv() {
118
+ const required = [
119
+ 'BC_API_BASE', 'BC_TENANT_ID', 'BC_ENVIRONMENT',
120
+ 'BC_TOKEN_URL', 'BC_CLIENT_ID', 'BC_CLIENT_SECRET', 'BC_SCOPE',
121
+ 'BC_COMPANY_FQ01',
122
+ ];
123
+ const missing = required.filter((k) => !process.env[k]);
124
+ if (missing.length > 0) {
125
+ console.error(`Missing required environment variables: ${missing.join(', ')}`);
126
+ console.error('Set them in your MCP client config or .env file.');
127
+ console.error('See: https://github.com/Fullqueso/fullqueso-mcp-bc-gastos#installation');
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ async function main() {
133
+ validateEnv();
134
+ const transport = new StdioServerTransport();
135
+ await server.connect(transport);
136
+ console.error('MCP Full Queso BC Gastos server running on stdio');
137
+ }
138
+
139
+ main().catch((error) => {
140
+ console.error('Fatal error:', error);
141
+ process.exit(1);
142
+ });
@@ -0,0 +1,125 @@
1
+ import { getExpenseCategory, getAccountName } from '../config/expense-accounts.js';
2
+ import { resolveStores } from '../config/company-config.js';
3
+ import { round2 } from '../utils/currency-converter.js';
4
+
5
+ export const accountTransactionsTool = {
6
+ name: 'get_account_transactions',
7
+ description:
8
+ 'Listado completo de transacciones para una cuenta contable específica con balance running y nombre de proveedor. Ideal para auditar movimientos de una cuenta, ej: "Todas las transacciones de la cuenta 68210 en FQ28 en diciembre 2025". Sin límite de registros. Montos en USD.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ account_number: {
13
+ type: 'string',
14
+ description: 'Número de cuenta contable (requerido), ej: "68210", "71100".',
15
+ },
16
+ store: {
17
+ type: 'string',
18
+ enum: ['FQ01', 'FQ28', 'FQ88'],
19
+ description: 'Tienda a consultar.',
20
+ },
21
+ start_date: {
22
+ type: 'string',
23
+ description: 'Fecha inicio en formato YYYY-MM-DD.',
24
+ },
25
+ end_date: {
26
+ type: 'string',
27
+ description: 'Fecha fin en formato YYYY-MM-DD.',
28
+ },
29
+ },
30
+ required: ['account_number', 'store', 'start_date', 'end_date'],
31
+ },
32
+ };
33
+
34
+ export async function handleAccountTransactions(bcClient, args) {
35
+ const { account_number, store, start_date, end_date } = args;
36
+
37
+ if (!account_number || !store || !start_date || !end_date) {
38
+ throw new Error('Parámetros requeridos: account_number, store, start_date, end_date');
39
+ }
40
+
41
+ const stores = resolveStores([store]);
42
+ const storeInfo = stores[0];
43
+
44
+ // Fetch GL entries (critical) and vendor map (best-effort) in parallel
45
+ const [entries, vendorMap] = await Promise.all([
46
+ bcClient.getDetailedGLEntries(storeInfo.companyId, {
47
+ startDate: start_date,
48
+ endDate: end_date,
49
+ accountNumber: account_number,
50
+ }),
51
+ bcClient.buildVendorMap(storeInfo.companyId, start_date, end_date),
52
+ ]);
53
+
54
+ // Get category and account info
55
+ const category = getExpenseCategory(account_number);
56
+ const accountName = getAccountName(account_number);
57
+
58
+ // Build transactions with running balance and vendor info
59
+ let runningBalance = 0;
60
+ const transactions = entries
61
+ .sort((a, b) => {
62
+ // Sort chronologically ascending for running balance
63
+ if (a.postingDate !== b.postingDate) return a.postingDate.localeCompare(b.postingDate);
64
+ return a.entryNumber - b.entryNumber;
65
+ })
66
+ .map((entry) => {
67
+ const debit = entry.debitAmount || 0;
68
+ const credit = entry.creditAmount || 0;
69
+ const netAmount = debit - credit;
70
+ runningBalance += netAmount;
71
+ const vendor = vendorMap[entry.documentNumber] || null;
72
+
73
+ return {
74
+ posting_date: entry.postingDate,
75
+ entry_number: entry.entryNumber,
76
+ document_no: entry.documentNumber,
77
+ description: entry.description || '',
78
+ vendor_name: vendor?.vendor_name || null,
79
+ vendor_number: vendor?.vendor_number || null,
80
+ debit: round2(debit),
81
+ credit: round2(credit),
82
+ net_amount: round2(netAmount),
83
+ running_balance: round2(runningBalance),
84
+ };
85
+ });
86
+
87
+ // Exchange rate
88
+ let exchangeRate = null;
89
+ try {
90
+ const rates = await bcClient.getExchangeRates(store);
91
+ exchangeRate = bcClient.getExchangeRateForDate(rates, end_date);
92
+ } catch (_) {
93
+ // optional
94
+ }
95
+
96
+ const totalDebit = round2(transactions.reduce((s, t) => s + t.debit, 0));
97
+ const totalCredit = round2(transactions.reduce((s, t) => s + t.credit, 0));
98
+ const totalNet = round2(totalDebit - totalCredit);
99
+
100
+ return {
101
+ store: storeInfo.name,
102
+ store_code: store,
103
+ account: {
104
+ number: account_number,
105
+ name: accountName,
106
+ category: category?.key || null,
107
+ category_name: category?.name || null,
108
+ category_icon: category?.icon || null,
109
+ },
110
+ period: { start: start_date, end: end_date },
111
+ exchange_rate: exchangeRate ? {
112
+ rate: round2(exchangeRate.rate),
113
+ label: `1 USD = ${round2(exchangeRate.rate)} VES`,
114
+ } : null,
115
+ summary: {
116
+ total_transactions: transactions.length,
117
+ total_debit: totalDebit,
118
+ total_credit: totalCredit,
119
+ net_amount: totalNet,
120
+ net_amount_ves: exchangeRate ? round2(totalNet * exchangeRate.rate) : null,
121
+ final_balance: transactions.length > 0 ? transactions[transactions.length - 1].running_balance : 0,
122
+ },
123
+ transactions,
124
+ };
125
+ }
@@ -0,0 +1,105 @@
1
+ import { getDateRange, getPreviousPeriodRange } from '../utils/date-helper.js';
2
+ import { detectAnomalies } from '../lib/anomaly-detector.js';
3
+
4
+ export const anomalyDetectionTool = {
5
+ name: 'detect_anomalies',
6
+ description:
7
+ 'Detecta anomalías en los gastos operacionales de Full Queso: gastos por encima de benchmarks, incrementos inusuales vs periodo anterior, concentración excesiva en una cuenta, y alertas de margen. Incluye severidad, causas posibles y acciones recomendadas.',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ period: {
12
+ type: 'string',
13
+ enum: ['last_7_days', 'last_30_days', 'this_month', 'last_month', 'last_3_months', 'specific_month', 'custom'],
14
+ description: 'Periodo a analizar para detectar anomalías.',
15
+ default: 'last_month',
16
+ },
17
+ month: {
18
+ type: 'string',
19
+ description: 'Mes específico en formato YYYY-MM. Usar con period="specific_month".',
20
+ },
21
+ start_date: {
22
+ type: 'string',
23
+ description: 'Fecha inicio (YYYY-MM-DD). Requerido si period=custom',
24
+ },
25
+ end_date: {
26
+ type: 'string',
27
+ description: 'Fecha fin (YYYY-MM-DD). Requerido si period=custom',
28
+ },
29
+ stores: {
30
+ type: 'array',
31
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
32
+ description: 'Tiendas a analizar.',
33
+ default: ['all'],
34
+ },
35
+ sensitivity: {
36
+ type: 'string',
37
+ enum: ['low', 'medium', 'high'],
38
+ description: 'Sensibilidad de detección. "high" detecta más anomalías.',
39
+ default: 'medium',
40
+ },
41
+ },
42
+ },
43
+ };
44
+
45
+ export async function handleAnomalyDetection(bcClient, args) {
46
+ const period = args.period || 'last_month';
47
+ const stores = args.stores || ['all'];
48
+
49
+ const dateRange = getDateRange(period, args.start_date, args.end_date, args.month);
50
+ const prevRange = getPreviousPeriodRange(dateRange.start, dateRange.end);
51
+
52
+ // Fetch current period
53
+ const currentData = await bcClient.getAllStoresFinancialData(stores, dateRange.start, dateRange.end);
54
+
55
+ // Fetch previous period for comparison
56
+ let previousData = null;
57
+ try {
58
+ previousData = await bcClient.getAllStoresFinancialData(stores, prevRange.start, prevRange.end);
59
+ } catch (err) {
60
+ // Previous period data is optional
61
+ }
62
+
63
+ // Exchange rate
64
+ const firstStoreCode = Object.keys(currentData)[0];
65
+ let exchangeRate = null;
66
+ try {
67
+ const rates = await bcClient.getExchangeRates(firstStoreCode);
68
+ exchangeRate = bcClient.getExchangeRateForDate(rates, dateRange.end);
69
+ } catch (err) {
70
+ // Optional
71
+ }
72
+
73
+ // Detect anomalies for each store
74
+ const results = {};
75
+ let totalAnomalies = 0;
76
+ let totalCritical = 0;
77
+ let totalWarnings = 0;
78
+
79
+ for (const [code, storeData] of Object.entries(currentData)) {
80
+ const prevStoreData = previousData ? previousData[code] : null;
81
+ results[code] = detectAnomalies(storeData, prevStoreData);
82
+ totalAnomalies += results[code].total_anomalies;
83
+ totalCritical += results[code].critical;
84
+ totalWarnings += results[code].warnings;
85
+ }
86
+
87
+ return {
88
+ period: {
89
+ current: { start: dateRange.start, end: dateRange.end },
90
+ previous: { start: prevRange.start, end: prevRange.end },
91
+ label: period,
92
+ },
93
+ exchange_rate: exchangeRate ? {
94
+ rate: Math.round(exchangeRate.rate * 100) / 100,
95
+ label: `1 USD = ${Math.round(exchangeRate.rate * 100) / 100} VES`,
96
+ } : null,
97
+ summary: {
98
+ total_anomalies: totalAnomalies,
99
+ critical: totalCritical,
100
+ warnings: totalWarnings,
101
+ status: totalCritical > 0 ? '🚨 Requiere atención inmediata' : totalWarnings > 0 ? '⚠️ Monitorear' : '✅ Sin anomalías significativas',
102
+ },
103
+ by_store: results,
104
+ };
105
+ }
@@ -0,0 +1,81 @@
1
+ import { getDateRange } from '../utils/date-helper.js';
2
+ import { calculateRatios } from '../lib/ratio-calculator.js';
3
+
4
+ export const efficiencyRatiosTool = {
5
+ name: 'get_efficiency_ratios',
6
+ description:
7
+ 'Calcula ratios de eficiencia financiera de Full Queso: gastos/ingresos, nómina/ingresos, alquiler/ingresos, servicios/ingresos, marketing/ingresos y margen operativo. Compara contra benchmarks del sector restaurantes/deli.',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ period: {
12
+ type: 'string',
13
+ enum: ['last_7_days', 'last_30_days', 'this_month', 'last_month', 'last_3_months', 'specific_month', 'custom'],
14
+ description: 'Periodo de análisis.',
15
+ default: 'last_month',
16
+ },
17
+ month: {
18
+ type: 'string',
19
+ description: 'Mes específico en formato YYYY-MM. Usar con period="specific_month".',
20
+ },
21
+ start_date: {
22
+ type: 'string',
23
+ description: 'Fecha inicio (YYYY-MM-DD). Requerido si period=custom',
24
+ },
25
+ end_date: {
26
+ type: 'string',
27
+ description: 'Fecha fin (YYYY-MM-DD). Requerido si period=custom',
28
+ },
29
+ stores: {
30
+ type: 'array',
31
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
32
+ description: 'Tiendas a analizar.',
33
+ default: ['all'],
34
+ },
35
+ },
36
+ },
37
+ };
38
+
39
+ export async function handleEfficiencyRatios(bcClient, args) {
40
+ const period = args.period || 'last_month';
41
+ const stores = args.stores || ['all'];
42
+
43
+ const dateRange = getDateRange(period, args.start_date, args.end_date, args.month);
44
+
45
+ const allStoresData = await bcClient.getAllStoresFinancialData(stores, dateRange.start, dateRange.end);
46
+
47
+ // Exchange rate
48
+ const firstStoreCode = Object.keys(allStoresData)[0];
49
+ let exchangeRate = null;
50
+ try {
51
+ const rates = await bcClient.getExchangeRates(firstStoreCode);
52
+ exchangeRate = bcClient.getExchangeRateForDate(rates, dateRange.end);
53
+ } catch (err) {
54
+ // Optional
55
+ }
56
+
57
+ const results = {};
58
+ for (const [code, storeData] of Object.entries(allStoresData)) {
59
+ results[code] = calculateRatios(storeData);
60
+ }
61
+
62
+ // Ranking by operating margin
63
+ const ranking = Object.entries(results)
64
+ .map(([code, r]) => ({
65
+ store: r.store,
66
+ store_code: code,
67
+ operating_margin_pct: r.main_ratios.operating_margin.value,
68
+ expense_to_income_pct: r.main_ratios.expense_to_income.value,
69
+ }))
70
+ .sort((a, b) => b.operating_margin_pct - a.operating_margin_pct);
71
+
72
+ return {
73
+ period: { start: dateRange.start, end: dateRange.end, label: period },
74
+ exchange_rate: exchangeRate ? {
75
+ rate: Math.round(exchangeRate.rate * 100) / 100,
76
+ label: `1 USD = ${Math.round(exchangeRate.rate * 100) / 100} VES`,
77
+ } : null,
78
+ by_store: results,
79
+ efficiency_ranking: ranking,
80
+ };
81
+ }
@@ -0,0 +1,88 @@
1
+ import { getDateRange } from '../utils/date-helper.js';
2
+ import { analyzeExpenses } from '../lib/expense-analyzer.js';
3
+ import { formatExpenseAnalysis } from '../lib/formatter.js';
4
+
5
+ export const expenseAnalysisTool = {
6
+ name: 'get_expense_analysis',
7
+ description:
8
+ 'Análisis detallado de gastos operacionales de Full Queso por categoría (nómina, alquiler, servicios, marketing, etc.) con números de cuenta específicos. Incluye comparación contra benchmarks del sector y insights accionables. Montos en USD (moneda base BC).',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ period: {
13
+ type: 'string',
14
+ enum: ['last_7_days', 'last_30_days', 'this_month', 'last_month', 'last_3_months', 'specific_month', 'custom'],
15
+ description: 'Periodo de análisis. Usar "specific_month" con el parámetro month para un mes específico.',
16
+ default: 'last_month',
17
+ },
18
+ month: {
19
+ type: 'string',
20
+ description: 'Mes específico en formato YYYY-MM (ej: "2026-01"). Usar con period="specific_month".',
21
+ },
22
+ start_date: {
23
+ type: 'string',
24
+ description: 'Fecha inicio (YYYY-MM-DD). Requerido si period=custom',
25
+ },
26
+ end_date: {
27
+ type: 'string',
28
+ description: 'Fecha fin (YYYY-MM-DD). Requerido si period=custom',
29
+ },
30
+ stores: {
31
+ type: 'array',
32
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
33
+ description: 'Tiendas a analizar. Use ["all"] para todas.',
34
+ default: ['all'],
35
+ },
36
+ },
37
+ },
38
+ };
39
+
40
+ export async function handleExpenseAnalysis(bcClient, args) {
41
+ const period = args.period || 'last_month';
42
+ const stores = args.stores || ['all'];
43
+
44
+ const dateRange = getDateRange(period, args.start_date, args.end_date, args.month);
45
+
46
+ // Fetch data for all requested stores
47
+ const allStoresData = await bcClient.getAllStoresFinancialData(stores, dateRange.start, dateRange.end);
48
+
49
+ // Fetch exchange rate for dual currency display
50
+ const firstStoreCode = Object.keys(allStoresData)[0];
51
+ let exchangeRate = null;
52
+ try {
53
+ const rates = await bcClient.getExchangeRates(firstStoreCode);
54
+ exchangeRate = bcClient.getExchangeRateForDate(rates, dateRange.end);
55
+ } catch (err) {
56
+ // Exchange rate is optional
57
+ }
58
+
59
+ const results = {};
60
+ for (const [code, storeData] of Object.entries(allStoresData)) {
61
+ results[code] = analyzeExpenses(storeData);
62
+ }
63
+
64
+ // If multiple stores, add combined summary
65
+ let combined = null;
66
+ const storeCodes = Object.keys(results);
67
+ if (storeCodes.length > 1) {
68
+ const totalIncome = storeCodes.reduce((s, c) => s + results[c].total_income, 0);
69
+ const totalExpenses = storeCodes.reduce((s, c) => s + results[c].total_expenses, 0);
70
+ combined = {
71
+ total_stores: storeCodes.length,
72
+ total_income: Math.round(totalIncome * 100) / 100,
73
+ total_expenses: Math.round(totalExpenses * 100) / 100,
74
+ operating_margin: Math.round((totalIncome - totalExpenses) * 100) / 100,
75
+ operating_margin_pct: totalIncome > 0 ? Math.round(((totalIncome - totalExpenses) / totalIncome) * 100 * 100) / 100 : 0,
76
+ };
77
+ }
78
+
79
+ return {
80
+ period: { start: dateRange.start, end: dateRange.end, label: period },
81
+ exchange_rate: exchangeRate ? {
82
+ rate: Math.round(exchangeRate.rate * 100) / 100,
83
+ label: `1 USD = ${Math.round(exchangeRate.rate * 100) / 100} VES`,
84
+ } : null,
85
+ by_store: results,
86
+ combined,
87
+ };
88
+ }