Skip to main content
Glama
index.ts20.4 kB
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'); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/radup/mcp-sigmund'

If you have feedback or need assistance with the MCP directory API, please join our Discord server