Skip to main content
Glama

MCP Memory Service

memory-scorer.js28.9 kB
/** * Memory Relevance Scoring Utility * Implements intelligent algorithms to score memories by relevance to current project context * Phase 2: Enhanced with conversation context awareness for dynamic memory loading */ /** * Calculate time decay factor for memory relevance * More recent memories get higher scores */ function calculateTimeDecay(memoryDate, decayRate = 0.1) { try { const now = new Date(); // Handle both Unix timestamps (seconds) and ISO strings let memoryTime; if (typeof memoryDate === 'string') { // ISO string format memoryTime = new Date(memoryDate); } else if (typeof memoryDate === 'number') { // Unix timestamp in seconds, convert to milliseconds memoryTime = new Date(memoryDate * 1000); } else { return 0.5; // Invalid format } if (isNaN(memoryTime.getTime())) { return 0.5; // Default score for invalid dates } // Calculate days since memory creation const daysDiff = (now - memoryTime) / (1000 * 60 * 60 * 24); // Exponential decay: score = e^(-decayRate * days) // Recent memories (0-7 days): score 0.8-1.0 // Older memories (8-30 days): score 0.3-0.8 // Ancient memories (30+ days): score 0.0-0.3 const decayScore = Math.exp(-decayRate * daysDiff); // Ensure score is between 0 and 1 return Math.max(0.01, Math.min(1.0, decayScore)); } catch (error) { // Silently fail with default score to avoid noise return 0.5; } } /** * Calculate tag relevance score * Memories with tags matching project context get higher scores */ function calculateTagRelevance(memoryTags = [], projectContext) { try { if (!Array.isArray(memoryTags) || memoryTags.length === 0) { return 0.3; // Default score for memories without tags } const contextTags = [ projectContext.name?.toLowerCase(), projectContext.language?.toLowerCase(), ...(projectContext.frameworks || []).map(f => f.toLowerCase()), ...(projectContext.tools || []).map(t => t.toLowerCase()) ].filter(Boolean); if (contextTags.length === 0) { return 0.5; // No context to match against } // Calculate tag overlap const memoryTagsLower = memoryTags.map(tag => tag.toLowerCase()); const matchingTags = contextTags.filter(contextTag => memoryTagsLower.some(memoryTag => memoryTag.includes(contextTag) || contextTag.includes(memoryTag) ) ); // Score based on percentage of matching tags const overlapScore = matchingTags.length / contextTags.length; // Bonus for exact project name matches const exactProjectMatch = memoryTagsLower.includes(projectContext.name?.toLowerCase()); const projectBonus = exactProjectMatch ? 0.3 : 0; // Bonus for exact language matches const exactLanguageMatch = memoryTagsLower.includes(projectContext.language?.toLowerCase()); const languageBonus = exactLanguageMatch ? 0.2 : 0; // Bonus for framework matches const frameworkMatches = (projectContext.frameworks || []).filter(framework => memoryTagsLower.some(tag => tag.includes(framework.toLowerCase())) ); const frameworkBonus = frameworkMatches.length * 0.1; const totalScore = Math.min(1.0, overlapScore + projectBonus + languageBonus + frameworkBonus); return Math.max(0.1, totalScore); } catch (error) { // Silently fail with default score to avoid noise return 0.3; } } /** * Calculate content quality score to penalize generic/empty content */ function calculateContentQuality(memoryContent = '') { try { if (!memoryContent || typeof memoryContent !== 'string') { return 0.1; } const content = memoryContent.trim(); // Check for generic session summary patterns const genericPatterns = [ /## 🎯 Topics Discussed\s*-\s*implementation\s*-\s*\.\.\.?$/m, /Topics Discussed.*implementation.*\.\.\..*$/s, /Session Summary.*implementation.*\.\.\..*$/s, /^# Session Summary.*Date.*Project.*Topics Discussed.*implementation.*\.\.\..*$/s ]; const isGeneric = genericPatterns.some(pattern => pattern.test(content)); if (isGeneric) { return 0.05; // Heavily penalize generic content } // Check content length and substance if (content.length < 50) { return 0.2; // Short content gets low score } // Check for meaningful content indicators const meaningfulIndicators = [ 'decided', 'implemented', 'changed', 'fixed', 'created', 'updated', 'because', 'reason', 'approach', 'solution', 'result', 'impact', 'learned', 'discovered', 'found', 'issue', 'problem', 'challenge' ]; const meaningfulMatches = meaningfulIndicators.filter(indicator => content.toLowerCase().includes(indicator) ).length; // Calculate information density const words = content.split(/\s+/).filter(w => w.length > 2); const uniqueWords = new Set(words.map(w => w.toLowerCase())); const diversityRatio = uniqueWords.size / Math.max(words.length, 1); // Combine factors const meaningfulnessScore = Math.min(0.4, meaningfulMatches * 0.08); const diversityScore = Math.min(0.3, diversityRatio * 0.5); const lengthScore = Math.min(0.3, content.length / 1000); // Longer content gets bonus const qualityScore = meaningfulnessScore + diversityScore + lengthScore; return Math.max(0.05, Math.min(1.0, qualityScore)); } catch (error) { // Silently fail with default score to avoid noise return 0.3; } } /** * Calculate content relevance using simple text analysis * Memories with content matching project keywords get higher scores */ function calculateContentRelevance(memoryContent = '', projectContext) { try { if (!memoryContent || typeof memoryContent !== 'string') { return 0.3; } const content = memoryContent.toLowerCase(); const keywords = [ projectContext.name?.toLowerCase(), projectContext.language?.toLowerCase(), ...(projectContext.frameworks || []).map(f => f.toLowerCase()), ...(projectContext.tools || []).map(t => t.toLowerCase()), // Add common technical keywords 'architecture', 'decision', 'implementation', 'bug', 'fix', 'feature', 'config', 'setup', 'deployment', 'performance' ].filter(Boolean); if (keywords.length === 0) { return 0.5; } // Count keyword occurrences let totalMatches = 0; let keywordScore = 0; keywords.forEach(keyword => { const occurrences = (content.match(new RegExp(keyword, 'g')) || []).length; if (occurrences > 0) { totalMatches++; keywordScore += Math.log(1 + occurrences) * 0.1; // Logarithmic scoring } }); // Normalize score const matchRatio = totalMatches / keywords.length; const contentScore = Math.min(1.0, matchRatio + keywordScore); return Math.max(0.1, contentScore); } catch (error) { // Silently fail with default score to avoid noise return 0.3; } } /** * Calculate memory type bonus * Certain memory types are more valuable for context injection */ function calculateTypeBonus(memoryType) { const typeScores = { 'decision': 0.3, // Architectural decisions are highly valuable 'architecture': 0.3, // Architecture documentation is important 'reference': 0.2, // Reference materials are useful 'session': 0.15, // Session summaries provide good context 'insight': 0.2, // Insights are valuable for learning 'bug-fix': 0.15, // Bug fixes provide historical context 'feature': 0.1, // Feature descriptions are moderately useful 'note': 0.05, // General notes are less critical 'todo': 0.05, // TODOs are task-specific 'temporary': -0.1 // Temporary notes should be deprioritized }; return typeScores[memoryType?.toLowerCase()] || 0; } /** * Calculate recency bonus to prioritize very recent memories * Provides explicit boost for memories created within specific time windows */ function calculateRecencyBonus(memoryDate) { // Recency bonus tiers (days and corresponding bonus values) const RECENCY_TIERS = [ { days: 7, bonus: 0.15 }, // Strong boost for last week { days: 14, bonus: 0.10 }, // Moderate boost for last 2 weeks { days: 30, bonus: 0.05 } // Small boost for last month ]; try { const now = new Date(); // Handle both Unix timestamps (seconds) and ISO strings let memoryTime; if (typeof memoryDate === 'string') { // ISO string format memoryTime = new Date(memoryDate); } else if (typeof memoryDate === 'number') { // Unix timestamp in seconds, convert to milliseconds memoryTime = new Date(memoryDate * 1000); } else { return 0; // Invalid format } if (isNaN(memoryTime.getTime()) || memoryTime > now) { return 0; // No bonus for invalid or future dates } const daysDiff = (now - memoryTime) / (1000 * 60 * 60 * 24); // Find the appropriate tier for this memory's age for (const tier of RECENCY_TIERS) { if (daysDiff <= tier.days) { return tier.bonus; } } return 0; // No bonus for older memories } catch (error) { return 0; } } /** * Calculate conversation context relevance score (Phase 2) * Matches memory content with current conversation topics and intent */ function calculateConversationRelevance(memory, conversationAnalysis) { try { if (!conversationAnalysis || !memory.content) { return 0.3; // Default score when no conversation context } const memoryContent = memory.content.toLowerCase(); let relevanceScore = 0; let factorCount = 0; // Score based on topic matching if (conversationAnalysis.topics && conversationAnalysis.topics.length > 0) { conversationAnalysis.topics.forEach(topic => { const topicMatches = (memoryContent.match(new RegExp(topic.name, 'gi')) || []).length; if (topicMatches > 0) { relevanceScore += topic.confidence * Math.min(topicMatches * 0.2, 0.8); factorCount++; } }); } // Score based on entity matching if (conversationAnalysis.entities && conversationAnalysis.entities.length > 0) { conversationAnalysis.entities.forEach(entity => { const entityMatches = (memoryContent.match(new RegExp(entity.name, 'gi')) || []).length; if (entityMatches > 0) { relevanceScore += entity.confidence * 0.3; factorCount++; } }); } // Score based on intent alignment if (conversationAnalysis.intent) { const intentKeywords = { 'learning': ['learn', 'understand', 'explain', 'how', 'tutorial', 'guide'], 'problem-solving': ['fix', 'error', 'debug', 'issue', 'problem', 'solve'], 'development': ['build', 'create', 'implement', 'develop', 'code', 'feature'], 'optimization': ['optimize', 'improve', 'performance', 'faster', 'better'], 'review': ['review', 'check', 'analyze', 'audit', 'validate'], 'planning': ['plan', 'design', 'architecture', 'approach', 'strategy'] }; const intentWords = intentKeywords[conversationAnalysis.intent.name] || []; let intentMatches = 0; intentWords.forEach(word => { if (memoryContent.includes(word)) { intentMatches++; } }); if (intentMatches > 0) { relevanceScore += conversationAnalysis.intent.confidence * (intentMatches / intentWords.length); factorCount++; } } // Score based on code context if present if (conversationAnalysis.codeContext && conversationAnalysis.codeContext.isCodeRelated) { const codeIndicators = ['code', 'function', 'class', 'method', 'variable', 'api', 'library']; let codeMatches = 0; codeIndicators.forEach(indicator => { if (memoryContent.includes(indicator)) { codeMatches++; } }); if (codeMatches > 0) { relevanceScore += 0.4 * (codeMatches / codeIndicators.length); factorCount++; } } // Normalize score const normalizedScore = factorCount > 0 ? relevanceScore / factorCount : 0.3; return Math.max(0.1, Math.min(1.0, normalizedScore)); } catch (error) { // Silently fail with default score to avoid noise return 0.3; } } /** * Calculate final relevance score for a memory (Enhanced with quality scoring) */ function calculateRelevanceScore(memory, projectContext, options = {}) { try { const { weights = {}, timeDecayRate = 0.1, // Default decay rate includeConversationContext = false, conversationAnalysis = null } = options; // Default weights including content quality factor const defaultWeights = includeConversationContext ? { timeDecay: 0.20, // Reduced weight for time tagRelevance: 0.30, // Tag matching remains important contentRelevance: 0.15, // Content matching reduced contentQuality: 0.25, // New quality factor conversationRelevance: 0.25, // Conversation context factor typeBonus: 0.05 // Memory type provides minor adjustment } : { timeDecay: 0.25, // Reduced time weight tagRelevance: 0.35, // Tag matching important contentRelevance: 0.15, // Content matching contentQuality: 0.25, // Quality factor prioritized typeBonus: 0.05 // Type bonus reduced }; const w = { ...defaultWeights, ...weights }; // Calculate individual scores const timeScore = calculateTimeDecay(memory.created_at || memory.created_at_iso, timeDecayRate); const tagScore = calculateTagRelevance(memory.tags, projectContext); const contentScore = calculateContentRelevance(memory.content, projectContext); const qualityScore = calculateContentQuality(memory.content); const typeBonus = calculateTypeBonus(memory.memory_type); const recencyBonus = calculateRecencyBonus(memory.created_at || memory.created_at_iso); let finalScore = ( (timeScore * w.timeDecay) + (tagScore * w.tagRelevance) + (contentScore * w.contentRelevance) + (qualityScore * w.contentQuality) + typeBonus + // Type bonus is not weighted, acts as adjustment recencyBonus // Recency bonus provides explicit boost for very recent memories ); const breakdown = { timeDecay: timeScore, tagRelevance: tagScore, contentRelevance: contentScore, contentQuality: qualityScore, typeBonus: typeBonus, recencyBonus: recencyBonus }; // Add conversation context scoring if enabled (Phase 2) if (includeConversationContext && conversationAnalysis) { const conversationScore = calculateConversationRelevance(memory, conversationAnalysis); finalScore += (conversationScore * (w.conversationRelevance || 0)); breakdown.conversationRelevance = conversationScore; } // Apply quality penalty for very low quality content (multiplicative) if (qualityScore < 0.2) { finalScore *= 0.5; // Heavily penalize low quality content } // Ensure score is between 0 and 1 const normalizedScore = Math.max(0, Math.min(1, finalScore)); return { finalScore: normalizedScore, breakdown: breakdown, weights: w, hasConversationContext: includeConversationContext }; } catch (error) { // Silently fail with default score to avoid noise return { finalScore: 0.1, breakdown: { error: error.message }, weights: {}, hasConversationContext: false }; } } /** * Score and sort memories by relevance */ function scoreMemoryRelevance(memories, projectContext, options = {}) { try { const { verbose = true } = options; if (!Array.isArray(memories)) { if (verbose) console.warn('[Memory Scorer] Invalid memories array'); return []; } if (verbose) { console.log(`[Memory Scorer] Scoring ${memories.length} memories for project: ${projectContext.name}`); } // Score each memory const scoredMemories = memories.map(memory => { const scoreResult = calculateRelevanceScore(memory, projectContext, options); return { ...memory, relevanceScore: scoreResult.finalScore, scoreBreakdown: scoreResult.breakdown, hasConversationContext: scoreResult.hasConversationContext }; }); // Sort by relevance score (highest first) const sortedMemories = scoredMemories.sort((a, b) => b.relevanceScore - a.relevanceScore); // Log scoring results for debugging if (verbose) { console.log('[Memory Scorer] Top scored memories:'); sortedMemories.slice(0, 3).forEach((memory, index) => { console.log(` ${index + 1}. Score: ${memory.relevanceScore.toFixed(3)} - ${memory.content.substring(0, 60)}...`); }); } return sortedMemories; } catch (error) { if (verbose) console.error('[Memory Scorer] Error scoring memories:', error.message); return memories || []; } } /** * Filter memories by minimum relevance threshold */ function filterByRelevance(memories, minScore = 0.3, options = {}) { try { const { verbose = true } = options; const filtered = memories.filter(memory => memory.relevanceScore >= minScore); if (verbose) { console.log(`[Memory Scorer] Filtered ${filtered.length}/${memories.length} memories above threshold ${minScore}`); } return filtered; } catch (error) { if (verbose) console.warn('[Memory Scorer] Error filtering memories:', error.message); return memories; } } /** * Analyze memory age distribution to detect staleness * Returns statistics and recommended weight adjustments */ function analyzeMemoryAgeDistribution(memories, options = {}) { try { const { verbose = false } = options; if (!Array.isArray(memories) || memories.length === 0) { return { avgAge: 0, medianAge: 0, p75Age: 0, p90Age: 0, recentCount: 0, staleCount: 0, isStale: false, recommendedAdjustments: {} }; } const now = new Date(); // Calculate ages in days const ages = memories.map(memory => { // Handle both Unix timestamps (seconds) and ISO strings let memoryTime; if (memory.created_at_iso) { memoryTime = new Date(memory.created_at_iso); } else if (memory.created_at) { // created_at is in seconds, convert to milliseconds memoryTime = new Date(memory.created_at * 1000); } else { return 365; // Default to very old if no timestamp } if (isNaN(memoryTime.getTime())) return 365; // Default to very old return (now - memoryTime) / (1000 * 60 * 60 * 24); }).sort((a, b) => a - b); // Calculate percentiles const avgAge = ages.reduce((sum, age) => sum + age, 0) / ages.length; const medianAge = ages[Math.floor(ages.length / 2)]; const p75Age = ages[Math.floor(ages.length * 0.75)]; const p90Age = ages[Math.floor(ages.length * 0.90)]; // Count recent vs stale const recentCount = ages.filter(age => age <= 14).length; // Last 2 weeks const staleCount = ages.filter(age => age > 30).length; // Older than 1 month // Determine if memory set is stale const isStale = medianAge > 30 || (recentCount / ages.length) < 0.2; // Recommended adjustments based on staleness const recommendedAdjustments = {}; if (isStale) { // Memories are old - boost time decay weight, reduce tag relevance recommendedAdjustments.timeDecay = 0.50; // Increase from default 0.25 recommendedAdjustments.tagRelevance = 0.20; // Decrease from default 0.35 recommendedAdjustments.recencyBonus = 0.25; // Increase bonus for any recent memories recommendedAdjustments.reason = `Stale memory set detected (median: ${Math.round(medianAge)}d old, ${Math.round(recentCount/ages.length*100)}% recent)`; } else if (avgAge < 14) { // Memories are very recent - balanced approach recommendedAdjustments.timeDecay = 0.30; recommendedAdjustments.tagRelevance = 0.30; recommendedAdjustments.reason = `Recent memory set (avg: ${Math.round(avgAge)}d old)`; } if (verbose) { console.log('[Memory Age Analyzer]', { avgAge: Math.round(avgAge), medianAge: Math.round(medianAge), p75Age: Math.round(p75Age), recentPercent: Math.round(recentCount / ages.length * 100), isStale, adjustments: recommendedAdjustments.reason || 'No adjustments needed' }); } return { avgAge, medianAge, p75Age, p90Age, recentCount, staleCount, totalCount: ages.length, isStale, recommendedAdjustments }; } catch (error) { if (verbose) console.error('[Memory Age Analyzer] Error:', error.message); return { avgAge: 0, medianAge: 0, p75Age: 0, p90Age: 0, recentCount: 0, staleCount: 0, isStale: false, recommendedAdjustments: {} }; } } /** * Calculate adaptive git context weight based on memory age and git activity * Prevents old git-related memories from dominating when recent development exists */ function calculateAdaptiveGitWeight(gitContext, memoryAgeAnalysis, configuredWeight = 1.2, options = {}) { try { const { verbose = false } = options; // No git context or no recent commits - use configured weight if (!gitContext || !gitContext.recentCommits || gitContext.recentCommits.length === 0) { return { weight: configuredWeight, reason: 'No recent git activity' }; } // Calculate days since most recent commit const now = new Date(); const mostRecentCommit = new Date(gitContext.recentCommits[0].date); const daysSinceLastCommit = (now - mostRecentCommit) / (1000 * 60 * 60 * 24); // Scenario 1: Recent commits (< 7d) BUT stale memories (median > 30d) // Problem: Git boost would amplify old git memories over potential recent work if (daysSinceLastCommit <= 7 && memoryAgeAnalysis.medianAge > 30) { const reducedWeight = Math.max(1.0, configuredWeight * 0.7); // Reduce by 30% const reason = `Recent commits (${Math.round(daysSinceLastCommit)}d ago) but stale memories (median: ${Math.round(memoryAgeAnalysis.medianAge)}d) - reducing git boost`; if (verbose) { console.log(`[Adaptive Git Weight] ${reason}: ${configuredWeight.toFixed(1)} → ${reducedWeight.toFixed(1)}`); } return { weight: reducedWeight, reason, adjusted: true }; } // Scenario 2: Both commits and memories are recent (< 14d) // Safe to use configured weight, git context is relevant if (daysSinceLastCommit <= 14 && memoryAgeAnalysis.avgAge <= 14) { return { weight: configuredWeight, reason: `Recent commits and memories aligned (${Math.round(daysSinceLastCommit)}d commits, ${Math.round(memoryAgeAnalysis.avgAge)}d avg memory age)`, adjusted: false }; } // Scenario 3: Old commits (> 14d) but recent memories exist // Slightly reduce git weight to let recent non-git memories surface if (daysSinceLastCommit > 14 && memoryAgeAnalysis.recentCount > 0) { const reducedWeight = Math.max(1.0, configuredWeight * 0.85); // Reduce by 15% const reason = `Older commits (${Math.round(daysSinceLastCommit)}d ago) with some recent memories - slightly reducing git boost`; if (verbose) { console.log(`[Adaptive Git Weight] ${reason}: ${configuredWeight.toFixed(1)} → ${reducedWeight.toFixed(1)}`); } return { weight: reducedWeight, reason, adjusted: true }; } // Default: use configured weight return { weight: configuredWeight, reason: 'Using configured weight', adjusted: false }; } catch (error) { if (verbose) console.error('[Adaptive Git Weight] Error:', error.message); return { weight: configuredWeight, reason: 'Error - using fallback', adjusted: false }; } } module.exports = { scoreMemoryRelevance, calculateRelevanceScore, calculateTimeDecay, calculateTagRelevance, calculateContentRelevance, calculateTypeBonus, calculateRecencyBonus, filterByRelevance, analyzeMemoryAgeDistribution, calculateAdaptiveGitWeight }; // Direct execution support for testing if (require.main === module) { // Test with mock data const mockProjectContext = { name: 'mcp-memory-service', language: 'JavaScript', frameworks: ['Node.js'], tools: ['npm'] }; const mockMemories = [ { content: 'Decided to use SQLite-vec for better performance in MCP Memory Service', tags: ['mcp-memory-service', 'decision', 'sqlite-vec'], memory_type: 'decision', created_at: '2025-08-19T10:00:00Z' }, { content: 'Fixed bug in JavaScript hook implementation for Claude Code integration', tags: ['javascript', 'bug-fix', 'claude-code'], memory_type: 'bug-fix', created_at: '2025-08-18T15:30:00Z' }, { content: 'Random note about completely unrelated project', tags: ['other-project', 'note'], memory_type: 'note', created_at: '2025-08-01T08:00:00Z' } ]; console.log('\n=== MEMORY SCORING TEST ==='); const scored = scoreMemoryRelevance(mockMemories, mockProjectContext); console.log('\n=== SCORED RESULTS ==='); scored.forEach((memory, index) => { console.log(`${index + 1}. Score: ${memory.relevanceScore.toFixed(3)}`); console.log(` Content: ${memory.content.substring(0, 80)}...`); console.log(` Breakdown:`, memory.scoreBreakdown); console.log(''); }); }

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/doobidoo/mcp-memory-service'

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