Skip to main content
Glama

MCP Memory Service

topic-change.js13.6 kB
/** * Claude Code Topic Change Hook * Monitors conversation flow and dynamically loads relevant memories when topics evolve * Phase 2: Intelligent Context Updates */ const fs = require('fs').promises; const path = require('path'); const https = require('https'); // Import utilities const { analyzeConversation, detectTopicChanges } = require('../utilities/conversation-analyzer'); const { scoreMemoryRelevance } = require('../utilities/memory-scorer'); const { formatMemoriesForContext } = require('../utilities/context-formatter'); // Global state for conversation tracking let conversationState = { previousAnalysis: null, loadedMemoryHashes: new Set(), sessionContext: null, topicChangeCount: 0 }; /** * Load hook configuration */ async function loadConfig() { try { const configPath = path.join(__dirname, '../config.json'); const configData = await fs.readFile(configPath, 'utf8'); return JSON.parse(configData); } catch (error) { console.warn('[Topic Change Hook] Using default configuration:', error.message); return { memoryService: { endpoint: 'https://10.0.1.30:8443', apiKey: 'test-key-123', maxMemoriesPerSession: 8 }, hooks: { topicChange: { enabled: true, timeout: 5000, priority: 'low', minSignificanceScore: 0.3, maxMemoriesPerUpdate: 3 } } }; } } /** * Query memory service for topic-specific memories */ async function queryMemoryService(endpoint, apiKey, query, options = {}) { return new Promise((resolve, reject) => { const { limit = 5, excludeHashes = [] } = options; const url = new URL('/mcp', endpoint); const postData = JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method: 'tools/call', params: { name: 'retrieve_memory', arguments: { query: query, limit: limit } } }); const requestOptions = { hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'Content-Length': Buffer.byteLength(postData) }, rejectUnauthorized: false, timeout: 5000 }; const req = https.request(requestOptions, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const response = JSON.parse(data); if (response.error) { console.error('[Topic Change Hook] Memory service error:', response.error); resolve([]); return; } // Parse memory results from response const memories = parseMemoryResults(response.result); // Filter out already loaded memories const filteredMemories = memories.filter(memory => !excludeHashes.includes(memory.content_hash) ); console.log(`[Topic Change Hook] Retrieved ${filteredMemories.length} new memories for topic query`); resolve(filteredMemories); } catch (parseError) { console.error('[Topic Change Hook] Failed to parse memory response:', parseError.message); resolve([]); } }); }); req.on('error', (error) => { console.error('[Topic Change Hook] Memory service request failed:', error.message); resolve([]); }); req.on('timeout', () => { console.error('[Topic Change Hook] Memory service request timed out'); req.destroy(); resolve([]); }); req.write(postData); req.end(); }); } /** * Parse memory results from MCP response */ function parseMemoryResults(result) { try { if (result && result.content && result.content[0] && result.content[0].text) { const text = result.content[0].text; // Try to extract results array from the response text const resultsMatch = text.match(/'results':\s*(\[[\s\S]*?\])/); if (resultsMatch) { // Use eval carefully on controlled content const resultsArray = eval(resultsMatch[1]); return resultsArray || []; } } return []; } catch (error) { console.error('[Topic Change Hook] Error parsing memory results:', error.message); return []; } } /** * Generate search queries from conversation analysis */ function generateTopicQueries(analysis, changes) { const queries = []; // Query for new topics changes.newTopics.forEach(topic => { queries.push({ query: topic.name, weight: topic.confidence, type: 'topic' }); }); // Query for current intent if changed if (changes.changedIntents && analysis.intent) { queries.push({ query: analysis.intent.name, weight: analysis.intent.confidence, type: 'intent' }); } // Query for high-confidence entities analysis.entities .filter(entity => entity.confidence > 0.7) .slice(0, 2) // Limit to top 2 entities .forEach(entity => { queries.push({ query: entity.name, weight: entity.confidence, type: 'entity' }); }); // Sort by weight and return top queries return queries .sort((a, b) => b.weight - a.weight) .slice(0, 3); // Limit to top 3 queries } /** * Format context update message */ function formatContextUpdate(memories, analysis, changes) { if (memories.length === 0) { return null; } let updateMessage = '\n🧠 **Dynamic Memory Context Update**\n\n'; // Explain why context is being updated if (changes.newTopics.length > 0) { updateMessage += `**New topics detected:** ${changes.newTopics.map(t => t.name).join(', ')}\n\n`; } if (changes.changedIntents) { updateMessage += `**Conversation focus shifted:** ${analysis.intent.name}\n\n`; } // Add relevant memories updateMessage += '**Additional relevant context:**\n'; memories.slice(0, 3).forEach((memory, index) => { const content = memory.content.length > 120 ? memory.content.substring(0, 120) + '...' : memory.content; updateMessage += `${index + 1}. ${content}\n`; if (memory.tags && memory.tags.length > 0) { updateMessage += ` *Tags: ${memory.tags.slice(0, 3).join(', ')}*\n`; } updateMessage += '\n'; }); updateMessage += '---\n'; return updateMessage; } /** * Main topic change detection and processing * @param {object} context - Conversation context */ async function onTopicChange(context) { console.log('[Topic Change Hook] Analyzing conversation for topic changes...'); try { const config = await loadConfig(); // Check if topic change hook is enabled if (!config.hooks?.topicChange?.enabled) { console.log('[Topic Change Hook] Hook is disabled, skipping'); return; } const { minSignificanceScore = 0.3, maxMemoriesPerUpdate = 3 } = config.hooks.topicChange; // Analyze current conversation const currentAnalysis = analyzeConversation(context.conversationText || '', { extractTopics: true, extractEntities: true, detectIntent: true, minTopicConfidence: 0.3 }); // Detect topic changes const changes = detectTopicChanges(conversationState.previousAnalysis, currentAnalysis); // Only proceed if significant topic change detected if (!changes.hasTopicShift || changes.significanceScore < minSignificanceScore) { console.log(`[Topic Change Hook] No significant topic change detected (score: ${changes.significanceScore.toFixed(2)})`); conversationState.previousAnalysis = currentAnalysis; return; } console.log(`[Topic Change Hook] Significant topic change detected (score: ${changes.significanceScore.toFixed(2)})`); console.log(`[Topic Change Hook] New topics: ${changes.newTopics.map(t => t.name).join(', ')}`); // Generate search queries for new topics const queries = generateTopicQueries(currentAnalysis, changes); if (queries.length === 0) { console.log('[Topic Change Hook] No actionable queries generated'); conversationState.previousAnalysis = currentAnalysis; return; } // Query memory service for each topic const allMemories = []; for (const queryObj of queries) { const memories = await queryMemoryService( config.memoryService.endpoint, config.memoryService.apiKey, queryObj.query, { limit: 2, excludeHashes: Array.from(conversationState.loadedMemoryHashes) } ); // Add query context to memories memories.forEach(memory => { memory.queryContext = queryObj; }); allMemories.push(...memories); } if (allMemories.length === 0) { console.log('[Topic Change Hook] No new relevant memories found'); conversationState.previousAnalysis = currentAnalysis; return; } // Score memories for relevance const projectContext = conversationState.sessionContext || { name: 'unknown' }; const scoredMemories = scoreMemoryRelevance(allMemories, projectContext, { includeConversationContext: true, conversationAnalysis: currentAnalysis }); // Select top memories for context update const selectedMemories = scoredMemories .filter(memory => memory.relevanceScore > 0.3) .slice(0, maxMemoriesPerUpdate); if (selectedMemories.length === 0) { console.log('[Topic Change Hook] No high-relevance memories found'); conversationState.previousAnalysis = currentAnalysis; return; } // Track loaded memories selectedMemories.forEach(memory => { conversationState.loadedMemoryHashes.add(memory.content_hash); }); // Format context update const contextUpdate = formatContextUpdate(selectedMemories, currentAnalysis, changes); if (contextUpdate) { // In a real implementation, this would inject the context into the conversation console.log('[Topic Change Hook] Context update generated:'); console.log(contextUpdate); // For now, we'll simulate the context injection if (context.onContextUpdate && typeof context.onContextUpdate === 'function') { context.onContextUpdate(contextUpdate); } } // Update conversation state conversationState.previousAnalysis = currentAnalysis; conversationState.topicChangeCount++; console.log(`[Topic Change Hook] Topic change processing completed (${conversationState.topicChangeCount} changes total)`); } catch (error) { console.error('[Topic Change Hook] Error processing topic change:', error.message); } } /** * Initialize topic change tracking for a new session * @param {object} sessionContext - Session context information */ function initializeTopicTracking(sessionContext) { console.log('[Topic Change Hook] Initializing topic tracking for new session'); conversationState = { previousAnalysis: null, loadedMemoryHashes: new Set(), sessionContext: sessionContext, topicChangeCount: 0 }; } /** * Reset topic tracking state */ function resetTopicTracking() { console.log('[Topic Change Hook] Resetting topic tracking state'); conversationState = { previousAnalysis: null, loadedMemoryHashes: new Set(), sessionContext: null, topicChangeCount: 0 }; } /** * Get current topic tracking statistics */ function getTopicTrackingStats() { return { topicChangeCount: conversationState.topicChangeCount, loadedMemoriesCount: conversationState.loadedMemoryHashes.size, hasSessionContext: !!conversationState.sessionContext, lastAnalysis: conversationState.previousAnalysis }; } module.exports = { onTopicChange, initializeTopicTracking, resetTopicTracking, getTopicTrackingStats };

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