import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import {
DatabaseClient,
closeDatabase,
checkDatabaseHealth,
startHealthMonitoring,
stopHealthMonitoring,
getQueryPerformanceMetrics,
clearQueryMetrics,
} from './database.js';
import {
handleMCPError,
validateDateRange,
validateLimit,
validateMonths,
validateProvider,
validateBankingQuery,
validateDataOperation,
sanitizeProviderId,
sanitizeAccountId,
sanitizeUserId,
sanitizeCategory,
sanitizeDateInput,
sanitizeNumericInput,
} from './error-handler.js';
import {
createSmartResponse,
} from './smart-formatting.js';
import { logInfo } from './logger.js';
import { BankingQueryResult } from './types.js';
// Text visualization functions are now defined locally
// Create server instance
const server = new McpServer({
name: 'MCP Sigmund',
version: '1.0.0',
capabilities: {
resources: {},
tools: {},
},
});
// Initialize database client
const dbClient = new DatabaseClient();
// Visualization functionality removed - keeping code simple and focused
// All HTML/SVG generation functions removed - keeping code simple
// Banking query tool - main tool for financial analysis
server.tool(
'banking_query',
'🏦 Banking data and analysis tool. Query accounts, transactions, balances, and financial insights from the database. When no provider is specified, queries ALL providers by default. When no account is specified, queries ALL accounts by default.',
{
query: z
.enum([
'accounts',
'transactions',
'balance',
'overview',
'spending_analysis',
'cashflow_analysis',
'providers',
])
.describe(
'Type of banking query to execute. Valid options: accounts (get account info), transactions (get transaction history), balance (get account balances), overview (get financial overview), spending_analysis (analyze spending by category), cashflow_analysis (analyze monthly cash flow), providers (list available banking providers)'
),
provider: z
.string()
.optional()
.describe(
'Banking provider to query (optional - defaults to ALL providers)'
),
params: z
.object({
limit: z
.number()
.optional()
.describe('Maximum number of results to return'),
dateFrom: z
.string()
.optional()
.describe('Start date for date range queries (YYYY-MM-DD)'),
dateTo: z
.string()
.optional()
.describe('End date for date range queries (YYYY-MM-DD)'),
months: z
.number()
.optional()
.describe('Number of months to analyze (default: 3)'),
category: z
.string()
.optional()
.describe('Filter by transaction category'),
accountId: z.string().optional().describe('Filter by account ID'),
userId: z.string().optional().describe('Filter by user ID'),
})
.optional()
.describe('Query-specific parameters'),
},
async ({ query, provider, params = {} }) => {
try {
console.error(
`MCP: Executing banking_query: ${query} for provider: ${provider || 'ALL'}`
);
// Validate and sanitize inputs
validateBankingQuery(query);
validateProvider(provider);
validateDateRange(params.dateFrom, params.dateTo);
validateLimit(params.limit);
validateMonths(params.months);
// Sanitize input parameters
const sanitizedProvider = provider
? sanitizeProviderId(provider)
: undefined;
const sanitizedParams = {
limit: params.limit
? sanitizeNumericInput(params.limit, 1, 10000)
: undefined,
dateFrom: params.dateFrom
? sanitizeDateInput(params.dateFrom)
: undefined,
dateTo: params.dateTo ? sanitizeDateInput(params.dateTo) : undefined,
months: params.months
? sanitizeNumericInput(params.months, 1, 60)
: undefined,
category: params.category
? sanitizeCategory(params.category)
: undefined,
accountId: params.accountId
? sanitizeAccountId(params.accountId)
: undefined,
userId: params.userId ? sanitizeUserId(params.userId) : undefined,
};
let result: BankingQueryResult;
switch (query) {
case 'accounts':
const accounts = await dbClient.getAccounts({
providerId: sanitizedProvider,
userId: sanitizedParams.userId,
});
result = {
success: true,
query: 'accounts',
provider: sanitizedProvider || 'all',
data: accounts,
timestamp: new Date().toISOString(),
};
break;
case 'transactions':
// Determine if we should simplify based on context
const shouldSimplify =
!sanitizedParams.limit ||
sanitizedParams.limit <= 10 ||
query.toLowerCase().includes('recent') ||
query.toLowerCase().includes('last');
const transactions = await dbClient.getTransactions({
limit: sanitizedParams.limit || 100,
dateFrom: sanitizedParams.dateFrom,
dateTo: sanitizedParams.dateTo,
category: sanitizedParams.category,
accountId: sanitizedParams.accountId,
providerId: sanitizedProvider,
userId: sanitizedParams.userId,
simplified: shouldSimplify,
});
result = {
success: true,
query: 'transactions',
provider: sanitizedProvider || 'all',
data: transactions,
timestamp: new Date().toISOString(),
};
break;
case 'balance':
const balances = await dbClient.getAccountBalances({
providerId: sanitizedProvider,
userId: sanitizedParams.userId,
});
result = {
success: true,
query: 'balance',
provider: sanitizedProvider || 'all',
accounts: balances,
summary: {
total_balance: balances.reduce(
(sum, acc) => sum + (acc.current_balance || 0),
0
),
currency: balances[0]?.currency || 'EUR',
provider_count: new Set(balances.map(b => b.provider_id)).size,
},
timestamp: new Date().toISOString(),
};
break;
case 'overview':
const overview = await dbClient.getFinancialOverview({
providerId: sanitizedProvider,
userId: sanitizedParams.userId,
});
result = {
success: true,
query: 'overview',
provider: sanitizedProvider || 'all',
data: overview,
timestamp: new Date().toISOString(),
};
break;
case 'spending_analysis':
const spendingData = await dbClient.getSpendingAnalysis({
months: sanitizedParams.months || 3,
category: sanitizedParams.category,
providerId: sanitizedProvider,
userId: sanitizedParams.userId,
});
result = {
success: true,
query: 'spending_analysis',
provider: sanitizedProvider || 'all',
spending_by_category: spendingData,
summary: {
total_spending: spendingData.reduce(
(sum, cat) => sum + cat.total_amount,
0
),
category_count: spendingData.length,
period_months: sanitizedParams.months || 3,
},
timestamp: new Date().toISOString(),
};
break;
case 'cashflow_analysis':
const monthlyData = await dbClient.getMonthlyData({
months: sanitizedParams.months || 3,
providerId: sanitizedProvider,
userId: sanitizedParams.userId,
});
result = {
success: true,
query: 'cashflow_analysis',
provider: sanitizedProvider || 'all',
monthly_cashflow: monthlyData,
summary: {
average_income:
monthlyData.length > 0
? monthlyData.reduce((sum, m) => sum + m.total_income, 0) /
monthlyData.length
: 0,
average_expenses:
monthlyData.length > 0
? monthlyData.reduce((sum, m) => sum + m.total_expenses, 0) /
monthlyData.length
: 0,
average_net_flow:
monthlyData.length > 0
? monthlyData.reduce((sum, m) => sum + m.net_flow, 0) /
monthlyData.length
: 0,
period_months: monthlyData.length,
},
timestamp: new Date().toISOString(),
};
break;
case 'providers':
const providers = await dbClient.getProviders();
result = {
success: true,
query: 'providers',
provider: 'all',
data: providers,
timestamp: new Date().toISOString(),
};
break;
default:
throw new Error(`Query '${query}' not supported`);
}
// Create smart response with display hints
const smartResponse = createSmartResponse(result, query, params);
// Build clean JSON response
const responseData = {
success: true,
query,
provider: provider || 'ALL',
...smartResponse,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(responseData, null, 2),
},
],
};
} catch (error) {
const errorResponse = handleMCPError(error, 'banking_query');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
...errorResponse,
query,
provider: provider || 'ALL',
},
null,
2
),
},
],
};
}
}
);
// Data query tool - for data management operations
server.tool(
'data_query',
'🗄️ Data management tool for cache operations, data export, validation, and cleanup.',
{
operation: z
.enum(['get_stats', 'validate_data', 'export_data', 'cleanup_data'])
.describe(
'Data operation to perform. Valid options: get_stats (get database statistics), validate_data (validate data integrity), export_data (export data in specified format), cleanup_data (clean up old or invalid data)'
),
target: z
.string()
.optional()
.describe('Target for the operation (e.g., specific table, data type)'),
params: z
.object({
format: z
.enum(['json', 'csv'])
.optional()
.describe(
'Export format. Valid options: json (JSON format), csv (CSV format)'
),
dateRange: z
.string()
.optional()
.describe(
"Date range for data operations (e.g., '2024-01-01 to 2024-12-31')"
),
})
.optional()
.describe('Operation-specific parameters'),
},
async ({ operation, target, params = {} }) => {
try {
console.error(
`MCP: Executing data_query: ${operation} for target: ${target || 'all'}`
);
// Validate inputs
validateDataOperation(operation);
let result: BankingQueryResult;
switch (operation) {
case 'get_stats':
const providers = await dbClient.getProviders();
const totalTransactions = providers.reduce(
(sum, p) => sum + p.transaction_count,
0
);
const totalAccounts = providers.reduce(
(sum, p) => sum + p.account_count,
0
);
result = {
success: true,
query: 'get_stats',
provider: 'all',
providers: providers.length,
total_transactions: totalTransactions,
total_accounts: totalAccounts,
largest_provider: providers[0]?.provider_name || 'N/A',
database_status: 'connected',
timestamp: new Date().toISOString(),
};
break;
case 'validate_data':
// Basic data validation
const transactions = await dbClient.getTransactions({ limit: 10 });
const accounts = await dbClient.getAccounts();
const monthlyData = await dbClient.getMonthlyData({ months: 1 });
result = {
success: true,
query: 'validate_data',
provider: 'all',
validation_status: 'passed',
transaction_sample_size: transactions.length,
account_count: accounts.length,
monthly_data_available: monthlyData.length > 0,
issues_found: [],
timestamp: new Date().toISOString(),
};
break;
case 'export_data':
const exportData = await dbClient.getTransactions({
limit: params.format === 'csv' ? 10000 : 1000,
dateFrom: params.dateRange?.split(' to ')[0],
dateTo: params.dateRange?.split(' to ')[1],
});
result = {
success: true,
query: 'export_data',
provider: 'all',
export_status: 'completed',
format: params.format || 'json',
record_count: exportData.length,
data: exportData,
timestamp: new Date().toISOString(),
};
break;
case 'cleanup_data':
result = {
success: true,
query: 'cleanup_data',
provider: 'all',
cleanup_status: 'completed',
message: 'Data cleanup operations completed successfully',
timestamp: new Date().toISOString(),
};
break;
default:
throw new Error(`Operation '${operation}' not supported`);
}
// Create smart response with display hints for data operations
const smartResponse = createSmartResponse(result, operation, params);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
operation,
target: target || 'all',
...smartResponse,
},
null,
2
),
},
],
};
} catch (error) {
const errorResponse = handleMCPError(error, 'data_query');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
...errorResponse,
operation,
target: target || 'all',
},
null,
2
),
},
],
};
}
}
);
// Graceful shutdown handling
let isShuttingDown = false;
async function gracefulShutdown(signal: string) {
if (isShuttingDown) {
console.error(`⚠️ Already shutting down, ignoring ${signal}`);
return;
}
isShuttingDown = true;
console.error(`🔄 Received ${signal}, starting graceful shutdown...`);
try {
// Stop health monitoring
stopHealthMonitoring();
// Close database connections
await closeDatabase();
console.error('✅ Graceful shutdown completed');
process.exit(0);
} catch (error) {
console.error('❌ Error during graceful shutdown:', error);
process.exit(1);
}
}
// Register shutdown handlers
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGQUIT', () => gracefulShutdown('SIGQUIT'));
// Handle uncaught exceptions
process.on('uncaughtException', error => {
console.error('❌ Uncaught Exception:', error);
gracefulShutdown('uncaughtException');
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason);
gracefulShutdown('unhandledRejection');
});
// Performance monitoring tool
server.tool(
'performance_monitor',
'📊 Performance monitoring tool for query metrics, system health, and performance analysis.',
{
operation: z
.enum(['get_metrics', 'clear_metrics', 'get_health', 'get_system_stats'])
.describe(
'Performance monitoring operation. Valid options: get_metrics (get query performance metrics), clear_metrics (clear performance metrics), get_health (get system health status), get_system_stats (get system resource usage)'
),
},
async ({ operation }) => {
try {
console.error(`MCP: Executing performance_monitor: ${operation}`);
let result: any;
switch (operation) {
case 'get_metrics':
result = getQueryPerformanceMetrics();
break;
case 'clear_metrics':
clearQueryMetrics();
result = {
status: 'success',
message: 'Performance metrics cleared successfully',
};
break;
case 'get_health':
const isHealthy = await checkDatabaseHealth();
result = {
database_healthy: isHealthy,
timestamp: new Date().toISOString(),
status: isHealthy ? 'healthy' : 'unhealthy',
};
break;
case 'get_system_stats':
const memUsage = process.memoryUsage();
result = {
memory: {
rss: Math.round((memUsage.rss / 1024 / 1024) * 100) / 100, // MB
heapTotal:
Math.round((memUsage.heapTotal / 1024 / 1024) * 100) / 100, // MB
heapUsed:
Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100, // MB
external:
Math.round((memUsage.external / 1024 / 1024) * 100) / 100, // MB
},
uptime: Math.round(process.uptime() * 100) / 100, // seconds
timestamp: new Date().toISOString(),
node_version: process.version,
platform: process.platform,
};
break;
default:
throw new Error(`Operation '${operation}' not supported`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
operation,
...result,
},
null,
2
),
},
],
};
} catch (error) {
const errorResponse = handleMCPError(error, 'performance_monitor');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
...errorResponse,
operation,
},
null,
2
),
},
],
};
}
}
);
async function main() {
try {
logInfo('Starting MCP Sigmund server...', 'server');
// Test database connection and health
const isHealthy = await checkDatabaseHealth();
if (!isHealthy) {
throw new Error('Database health check failed');
}
await dbClient.getProviders();
logInfo('Database connection established', 'server');
// Start health monitoring
startHealthMonitoring(30000); // Check every 30 seconds
const transport = new StdioServerTransport();
await server.connect(transport);
logInfo('MCP Sigmund server running on stdio', 'server');
logInfo('Available tools: banking_query, data_query', 'server');
logInfo('Default behavior: ALL providers when none specified', 'server');
} catch (error) {
console.error('Fatal error in main():', error);
await gracefulShutdown('startup_error');
}
}
main().catch(error => {
console.error('Fatal error in main():', error);
gracefulShutdown('main_error');
});