Skip to main content
Glama

meMCP - Memory-Enhanced Model Context Protocol

MIT License
23
2
AnalyticsManager.js12.1 kB
import { promises as fs } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; export class AnalyticsManager { constructor(factStore, configManager) { this.factStore = factStore; this.configManager = configManager; this.analyticsDir = join(homedir(), '.mcp_sequential_thinking', 'analytics'); this.analyticsFile = join(this.analyticsDir, 'analytics.json'); this.metrics = { factsStored: 0, factsRetrieved: 0, queriesExecuted: 0, sessionCount: 0, averageQualityScore: 0, toolUsage: {}, domainActivity: {}, factTypeDistribution: {}, qualityTrends: [], performanceMetrics: {}, retentionMetrics: {}, }; this.sessionMetrics = { startTime: null, factsProcessed: 0, queriesExecuted: 0, toolsUsed: new Set(), }; this.initialized = false; } async initialize() { try { await this.ensureAnalyticsDirectory(); await this.loadExistingAnalytics(); this.startSession(); this.initialized = true; console.log('AnalyticsManager initialized'); } catch (error) { console.error('Failed to initialize AnalyticsManager:', error); throw error; } } async ensureAnalyticsDirectory() { try { await fs.mkdir(this.analyticsDir, { recursive: true }); } catch (error) { console.error('Failed to create analytics directory:', error); throw error; } } async loadExistingAnalytics() { try { const data = await fs.readFile(this.analyticsFile, 'utf-8'); const loadedMetrics = JSON.parse(data); this.metrics = { ...this.metrics, ...loadedMetrics }; } catch (error) { console.log('No existing analytics found, starting fresh'); } } async saveAnalytics() { try { const analyticsData = { ...this.metrics, lastUpdated: new Date().toISOString(), version: '1.0.0', }; await fs.writeFile(this.analyticsFile, JSON.stringify(analyticsData, null, 2)); } catch (error) { console.error('Failed to save analytics:', error); } } startSession() { this.sessionMetrics = { startTime: new Date(), factsProcessed: 0, queriesExecuted: 0, toolsUsed: new Set(), }; this.metrics.sessionCount += 1; } async endSession() { if (!this.sessionMetrics.startTime) return; const sessionDuration = Date.now() - this.sessionMetrics.startTime.getTime(); this.recordPerformanceMetric('sessionDuration', sessionDuration); this.recordPerformanceMetric('factsPerSession', this.sessionMetrics.factsProcessed); this.recordPerformanceMetric('queriesPerSession', this.sessionMetrics.queriesExecuted); await this.saveAnalytics(); console.log(`Session ended: ${this.sessionMetrics.factsProcessed} facts processed, ${this.sessionMetrics.queriesExecuted} queries executed`); } recordFactStored(fact) { this.metrics.factsStored += 1; this.sessionMetrics.factsProcessed += 1; this.updateFactTypeDistribution(fact.type); this.updateDomainActivity(fact.domain); this.updateQualityTrends(fact.qualityScore); this.scheduleAnalyticsSave(); } recordFactRetrieved(facts) { this.metrics.factsRetrieved += facts.length; for (const fact of facts) { this.updateDomainActivity(fact.domain); this.updateFactTypeDistribution(fact.type); } } recordQueryExecuted(query, results) { this.metrics.queriesExecuted += 1; this.sessionMetrics.queriesExecuted += 1; this.recordPerformanceMetric('queryResultCount', results.facts.length); this.recordPerformanceMetric('queryExecutionTime', results.executionTime || 0); if (query.type) { this.updateFactTypeDistribution(query.type); } if (query.domain) { this.updateDomainActivity(query.domain); } } recordToolUsage(toolName, duration = 0) { if (!this.metrics.toolUsage[toolName]) { this.metrics.toolUsage[toolName] = { count: 0, totalDuration: 0, averageDuration: 0, }; } this.metrics.toolUsage[toolName].count += 1; this.metrics.toolUsage[toolName].totalDuration += duration; this.metrics.toolUsage[toolName].averageDuration = this.metrics.toolUsage[toolName].totalDuration / this.metrics.toolUsage[toolName].count; this.sessionMetrics.toolsUsed.add(toolName); } updateFactTypeDistribution(factType) { if (!this.metrics.factTypeDistribution[factType]) { this.metrics.factTypeDistribution[factType] = 0; } this.metrics.factTypeDistribution[factType] += 1; } updateDomainActivity(domain) { if (!domain) return; if (!this.metrics.domainActivity[domain]) { this.metrics.domainActivity[domain] = { factsStored: 0, factsRetrieved: 0, lastActivity: null, }; } this.metrics.domainActivity[domain].factsStored += 1; this.metrics.domainActivity[domain].lastActivity = new Date().toISOString(); } updateQualityTrends(qualityScore) { const now = new Date(); const dateKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; const existingEntry = this.metrics.qualityTrends.find(entry => entry.date === dateKey); if (existingEntry) { existingEntry.scores.push(qualityScore); existingEntry.average = existingEntry.scores.reduce((a, b) => a + b, 0) / existingEntry.scores.length; } else { this.metrics.qualityTrends.push({ date: dateKey, scores: [qualityScore], average: qualityScore, count: 1, }); } if (this.metrics.qualityTrends.length > 30) { this.metrics.qualityTrends = this.metrics.qualityTrends.slice(-30); } this.updateAverageQualityScore(); } updateAverageQualityScore() { if (this.factStore.factsIndex.size === 0) { this.metrics.averageQualityScore = 0; return; } const totalScore = Array.from(this.factStore.factsIndex.values()) .reduce((sum, fact) => sum + (fact.qualityScore || 0), 0); this.metrics.averageQualityScore = Math.round(totalScore / this.factStore.factsIndex.size); } recordPerformanceMetric(metricName, value) { if (!this.metrics.performanceMetrics[metricName]) { this.metrics.performanceMetrics[metricName] = { count: 0, total: 0, average: 0, min: Number.MAX_SAFE_INTEGER, max: 0, }; } const metric = this.metrics.performanceMetrics[metricName]; metric.count += 1; metric.total += value; metric.average = metric.total / metric.count; metric.min = Math.min(metric.min, value); metric.max = Math.max(metric.max, value); } async calculateRetentionMetrics() { const allFacts = Array.from(this.factStore.factsIndex.values()); const now = new Date(); const retentionPeriods = [7, 30, 90, 365]; const retentionData = {}; for (const days of retentionPeriods) { const cutoffDate = new Date(now.getTime() - (days * 24 * 60 * 60 * 1000)); const factsInPeriod = allFacts.filter(fact => { const factDate = new Date(fact.createdAt || fact.metadata?.processedAt); return factDate >= cutoffDate; }); const accessedFacts = factsInPeriod.filter(fact => fact.lastAccessed && new Date(fact.lastAccessed) >= cutoffDate ); retentionData[`${days}days`] = { totalFacts: factsInPeriod.length, accessedFacts: accessedFacts.length, retentionRate: factsInPeriod.length > 0 ? accessedFacts.length / factsInPeriod.length : 0, }; } this.metrics.retentionMetrics = retentionData; return retentionData; } async generateInsights() { await this.calculateRetentionMetrics(); const insights = []; if (this.metrics.averageQualityScore < 60) { insights.push({ type: 'quality_concern', message: `Average quality score is ${this.metrics.averageQualityScore}/100. Consider reviewing fact processing criteria.`, severity: 'medium', }); } const recentTrend = this.metrics.qualityTrends.slice(-7); if (recentTrend.length >= 3) { const isDecreasing = recentTrend.every((entry, index) => index === 0 || entry.average < recentTrend[index - 1].average ); if (isDecreasing) { insights.push({ type: 'quality_decline', message: 'Quality scores have been declining over the past week.', severity: 'high', }); } } const mostUsedDomain = Object.entries(this.metrics.domainActivity) .sort(([, a], [, b]) => b.factsStored - a.factsStored)[0]; if (mostUsedDomain && mostUsedDomain[1].factsStored > this.metrics.factsStored * 0.5) { insights.push({ type: 'domain_concentration', message: `${mostUsedDomain[1].factsStored}% of facts are in ${mostUsedDomain[0]} domain. Consider diversifying knowledge capture.`, severity: 'low', }); } const lowRetentionPeriods = Object.entries(this.metrics.retentionMetrics) .filter(([, data]) => data.retentionRate < 0.3); if (lowRetentionPeriods.length > 0) { insights.push({ type: 'low_retention', message: `Low fact access rates detected in: ${lowRetentionPeriods.map(([period]) => period).join(', ')}`, severity: 'medium', }); } return insights; } async getAnalyticsReport() { const insights = await this.generateInsights(); return { summary: { totalFacts: this.factStore.factsIndex.size, factsStored: this.metrics.factsStored, factsRetrieved: this.metrics.factsRetrieved, queriesExecuted: this.metrics.queriesExecuted, sessionCount: this.metrics.sessionCount, averageQualityScore: this.metrics.averageQualityScore, }, usage: { toolUsage: this.metrics.toolUsage, domainActivity: this.metrics.domainActivity, factTypeDistribution: this.metrics.factTypeDistribution, }, trends: { qualityTrends: this.metrics.qualityTrends.slice(-14), }, performance: this.metrics.performanceMetrics, retention: this.metrics.retentionMetrics, insights, generatedAt: new Date().toISOString(), }; } scheduleAnalyticsSave() { if (this.saveTimeout) { clearTimeout(this.saveTimeout); } this.saveTimeout = setTimeout(() => { this.saveAnalytics(); }, 30000); } async exportAnalytics(format = 'json') { const report = await this.getAnalyticsReport(); if (format === 'json') { return JSON.stringify(report, null, 2); } if (format === 'csv') { return this.convertToCSV(report); } throw new Error(`Unsupported export format: ${format}`); } convertToCSV(report) { const lines = []; lines.push('Metric,Value'); lines.push(`Total Facts,${report.summary.totalFacts}`); lines.push(`Facts Stored,${report.summary.factsStored}`); lines.push(`Facts Retrieved,${report.summary.factsRetrieved}`); lines.push(`Queries Executed,${report.summary.queriesExecuted}`); lines.push(`Average Quality Score,${report.summary.averageQualityScore}`); lines.push(''); lines.push('Domain,Facts Stored'); for (const [domain, data] of Object.entries(report.usage.domainActivity)) { lines.push(`${domain},${data.factsStored}`); } lines.push(''); lines.push('Fact Type,Count'); for (const [type, count] of Object.entries(report.usage.factTypeDistribution)) { lines.push(`${type},${count}`); } return lines.join('\n'); } async shutdown() { if (this.initialized) { await this.endSession(); await this.saveAnalytics(); console.log('AnalyticsManager shut down successfully'); } } }

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/mixelpixx/meMCP'

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