Skip to main content
Glama
hiltonbrown

Next.js MCP Server Template

by hiltonbrown
monitoring.ts10.1 kB
// Monitoring and logging system for Xero MCP integration import { db } from './db'; export interface MonitoringEvent { id: string; timestamp: Date; eventType: string; category: 'mcp' | 'xero' | 'auth' | 'system' | 'user'; userId?: string; sessionId?: string; data: Record<string, any>; duration?: number; success: boolean; error?: string; } export interface PerformanceMetric { name: string; value: number; unit: 'ms' | 'bytes' | 'count'; timestamp: Date; tags?: Record<string, string>; } export class MonitoringService { private static instance: MonitoringService; private events: MonitoringEvent[] = []; private metrics: PerformanceMetric[] = []; static getInstance(): MonitoringService { if (!MonitoringService.instance) { MonitoringService.instance = new MonitoringService(); } return MonitoringService.instance; } // Log MCP tool usage async logMCPToolUsage( toolName: string, userId: string, sessionId: string, success: boolean, duration: number, data?: any, error?: string ) { const event: MonitoringEvent = { id: `mcp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), eventType: 'tool_execution', category: 'mcp', userId, sessionId, data: { toolName, ...data }, duration, success, error }; await this.logEvent(event); // Record performance metric this.recordMetric('mcp_tool_execution_time', duration, 'ms', { tool: toolName, success: success.toString() }); } // Log Xero API calls async logXeroAPICall( endpoint: string, method: string, userId: string, success: boolean, duration: number, statusCode?: number, error?: string ) { const event: MonitoringEvent = { id: `xero_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), eventType: 'api_call', category: 'xero', userId, data: { endpoint, method, statusCode }, duration, success, error }; await this.logEvent(event); // Record performance metric this.recordMetric('xero_api_call_time', duration, 'ms', { endpoint, method, success: success.toString() }); } // Log authentication events async logAuthEvent( eventType: 'login' | 'logout' | 'token_refresh' | 'oauth_callback', userId: string, success: boolean, data?: any, error?: string ) { const event: MonitoringEvent = { id: `auth_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), eventType, category: 'auth', userId, data, success, error }; await this.logEvent(event); } // Log system events async logSystemEvent( eventType: string, data: any, severity: 'info' | 'warning' | 'error' = 'info' ) { const event: MonitoringEvent = { id: `system_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), eventType, category: 'system', data: { ...data, severity }, success: severity !== 'error' }; await this.logEvent(event); } // Record performance metrics recordMetric( name: string, value: number, unit: 'ms' | 'bytes' | 'count', tags?: Record<string, string> ) { const metric: PerformanceMetric = { name, value, unit, timestamp: new Date(), tags }; this.metrics.unshift(metric); // Keep only last 1000 metrics in memory if (this.metrics.length > 1000) { this.metrics = this.metrics.slice(0, 1000); } // Log to console in development if (process.env.NODE_ENV === 'development') { console.log(`📊 Metric: ${name} = ${value}${unit}`, tags); } } // Private method to log events private async logEvent(event: MonitoringEvent) { // Store in memory for quick access this.events.unshift(event); if (this.events.length > 1000) { this.events = this.events.slice(0, 1000); } // Log to console with appropriate formatting const logData = { id: event.id, timestamp: event.timestamp.toISOString(), type: event.eventType, category: event.category, userId: event.userId, sessionId: event.sessionId, success: event.success, duration: event.duration, error: event.error }; if (event.success) { console.log(`✅ ${event.category.toUpperCase()}: ${event.eventType}`, logData); } else { console.error(`❌ ${event.category.toUpperCase()}: ${event.eventType}`, logData); } // In production, persist to database or external monitoring service if (process.env.NODE_ENV === 'production') { try { // Store in database for long-term analysis await this.persistEvent(event); // Send to external monitoring service await this.sendToExternalMonitoring(event); } catch (error) { console.error('Failed to persist monitoring event:', error); } } } // Persist event to database private async persistEvent(event: MonitoringEvent) { try { // Store in a monitoring_events table (would need to create this schema) // For now, we'll just log that we would persist it console.log('📝 Would persist event to database:', event.id); } catch (error) { console.error('Failed to persist event:', error); } } // Send to external monitoring service private async sendToExternalMonitoring(event: MonitoringEvent) { // Send to services like Sentry, DataDog, etc. if (process.env.SENTRY_DSN) { // Send to Sentry console.log('📤 Would send to Sentry:', event.id); } if (process.env.DATADOG_API_KEY) { // Send to DataDog console.log('📤 Would send to DataDog:', event.id); } } // Get monitoring statistics getStats(timeRange: '1h' | '24h' | '7d' = '24h') { const now = new Date(); const timeRangeMs = { '1h': 60 * 60 * 1000, '24h': 24 * 60 * 60 * 1000, '7d': 7 * 24 * 60 * 60 * 1000 }; const cutoff = new Date(now.getTime() - timeRangeMs[timeRange]); const relevantEvents = this.events.filter(e => e.timestamp >= cutoff); const relevantMetrics = this.metrics.filter(m => m.timestamp >= cutoff); const stats = { timeRange, totalEvents: relevantEvents.length, totalMetrics: relevantMetrics.length, eventsByCategory: {} as Record<string, number>, eventsByType: {} as Record<string, number>, successRate: 0, averageDuration: 0, errorRate: 0, topTools: [] as Array<{ name: string; count: number }>, recentEvents: relevantEvents.slice(0, 10) }; // Calculate statistics let totalDuration = 0; let durationCount = 0; let successCount = 0; const toolUsage: Record<string, number> = {}; relevantEvents.forEach(event => { // Category counts stats.eventsByCategory[event.category] = (stats.eventsByCategory[event.category] || 0) + 1; // Type counts stats.eventsByType[event.eventType] = (stats.eventsByType[event.eventType] || 0) + 1; // Success rate if (event.success) successCount++; // Duration tracking if (event.duration) { totalDuration += event.duration; durationCount++; } // Tool usage tracking if (event.category === 'mcp' && event.data?.toolName) { toolUsage[event.data.toolName] = (toolUsage[event.data.toolName] || 0) + 1; } }); stats.successRate = relevantEvents.length > 0 ? (successCount / relevantEvents.length) * 100 : 0; stats.averageDuration = durationCount > 0 ? totalDuration / durationCount : 0; stats.errorRate = 100 - stats.successRate; // Top tools stats.topTools = Object.entries(toolUsage) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([name, count]) => ({ name, count })); return stats; } // Health check metrics async getHealthMetrics() { const stats = this.getStats('1h'); return { status: stats.errorRate < 5 ? 'healthy' : stats.errorRate < 15 ? 'warning' : 'critical', uptime: process.uptime(), memory: process.memoryUsage(), eventsPerHour: stats.totalEvents, errorRate: stats.errorRate, averageResponseTime: stats.averageDuration, timestamp: new Date().toISOString() }; } // Clear monitoring data clearData() { this.events = []; this.metrics = []; } } // Export singleton instance export const monitoring = MonitoringService.getInstance(); // Utility functions for easy monitoring export const monitorMCPTool = async ( toolName: string, userId: string, sessionId: string, operation: () => Promise<any> ) => { const startTime = Date.now(); try { const result = await operation(); const duration = Date.now() - startTime; await monitoring.logMCPToolUsage(toolName, userId, sessionId, true, duration, result); return result; } catch (error) { const duration = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; await monitoring.logMCPToolUsage(toolName, userId, sessionId, false, duration, undefined, errorMessage); throw error; } }; export const monitorXeroAPI = async ( endpoint: string, method: string, userId: string, operation: () => Promise<any> ) => { const startTime = Date.now(); try { const result = await operation(); const duration = Date.now() - startTime; await monitoring.logXeroAPICall(endpoint, method, userId, true, duration, result.status); return result; } catch (error) { const duration = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; await monitoring.logXeroAPICall(endpoint, method, userId, false, duration, undefined, errorMessage); throw 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/hiltonbrown/xero-mcp-with-next-js'

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