UNPKG

mongodb-memory-bank-mcp-v2

Version:

MongoDB-powered Memory Bank MCP server with hybrid search capabilities for AI assistants

354 lines (341 loc) โ€ข 14.2 kB
import { join } from 'path'; import { readFileSync, existsSync } from 'fs'; import { SearchToolSchema } from '../types/memory.js'; import { getMemoryCollection } from '../db/connection.js'; import { generateEmbedding } from '../embeddings/voyage.js'; import { logger } from '../utils/logger.js'; export async function searchTool(args) { try { const params = SearchToolSchema.parse(args); const projectPath = params.projectPath || process.cwd(); // Read project config const configPath = join(projectPath, '.memory-bank', 'config.json'); if (!existsSync(configPath)) { return { content: [ { type: 'text', text: 'Memory bank not initialized for this project. Run memory_bank/init first.', }, ], }; } const config = JSON.parse(readFileSync(configPath, 'utf-8')); const collection = getMemoryCollection(); let results; if (params.searchType === 'text') { // Text-only search using Atlas Search results = await collection .aggregate([ { $search: { index: 'memory_text_index', text: { query: params.query, path: ['content', 'fileName'], }, }, }, { $match: { projectId: config.projectId, }, }, { $project: { fileName: 1, content: 1, 'metadata.type': 1, score: { $meta: 'searchScore' }, }, }, { $limit: params.limit, }, ]) .toArray(); } else if (params.searchType === 'vector') { // Vector-only search const queryVector = await generateEmbedding(params.query); // Check if vector index exists const hasVectorData = await collection.findOne({ projectId: config.projectId, contentVector: { $exists: true, $not: { $type: 'null' } }, }); if (!hasVectorData) { return { content: [ { type: 'text', text: 'No embeddings found. Run memory_bank/sync to generate embeddings first.', }, ], }; } // MongoDB Atlas Vector Search aggregation // Use without filter and then match - works with basic vector index results = await collection .aggregate([ { $vectorSearch: { index: 'memory_vector_index', path: 'contentVector', queryVector: queryVector, numCandidates: params.limit * 10, limit: params.limit * 2, }, }, { $match: { projectId: config.projectId, }, }, { $project: { fileName: 1, content: 1, 'metadata.type': 1, score: { $meta: 'vectorSearchScore' }, }, }, { $limit: params.limit, }, ]) .toArray(); } else { // Hybrid search using MongoDB's native $rankFusion const queryVector = await generateEmbedding(params.query); // Check if vector index exists const hasVectorData = await collection.findOne({ projectId: config.projectId, contentVector: { $exists: true, $not: { $type: 'null' } }, }); if (!hasVectorData) { // Fall back to text search only logger.warn('No embeddings found, falling back to text search'); params.searchType = 'text'; return searchTool(params); } // MongoDB native hybrid search with $rankFusion results = await collection .aggregate([ { $rankFusion: { input: { pipelines: { vectorPipeline: [ { $vectorSearch: { index: 'memory_vector_index', path: 'contentVector', queryVector: queryVector, numCandidates: params.limit * 10, limit: params.limit, }, }, { $match: { projectId: config.projectId, }, }, ], textPipeline: [ { $search: { index: 'memory_text_index', text: { query: params.query, path: ['content', 'fileName'], }, }, }, { $match: { projectId: config.projectId, }, }, { $limit: params.limit, }, ], }, }, combination: { weights: { vectorPipeline: 0.7, textPipeline: 0.3, }, }, }, }, { $addFields: { score: { $meta: 'searchScore' }, // RRF score from $rankFusion }, }, { $project: { fileName: 1, content: 1, 'metadata.type': 1, score: 1, }, }, { $limit: params.limit, }, ]) .toArray(); } if (!results || results.length === 0) { return { content: [ { type: 'text', text: `๐Ÿ” No results found for query: "${params.query}" ๐Ÿ’ก OPTIMIZATION TIPS: 1. ๐Ÿ”„ Run memory_bank/sync to generate/update embeddings 2. ๐ŸŽฏ Try different keywords or concepts 3. ๐Ÿš€ Use hybrid search (default) for best results! ${params.searchType === 'hybrid' ? `๐Ÿ’Ž MongoDB $rankFusion searched BOTH: - Semantic meaning (what you're looking for conceptually) - Exact keywords (what you typed) No matches means this is truly NEW territory!` : `๐Ÿ”ง You used ${params.searchType} search only. Try 'hybrid' for MongoDB's FULL POWER!`}`, }, ], }; } logger.info(`Search completed: ${results.length} results for query "${params.query}"`); // Helper function to extract relevant context around search terms const getSmartPreview = (content, query) => { const lowerContent = content.toLowerCase(); const lowerQuery = query.toLowerCase(); const words = lowerQuery.split(/\s+/); // Find the best matching section let bestStart = 0; let bestScore = 0; // Check each position in the content for (let i = 0; i < content.length - 150; i += 50) { const section = lowerContent.substring(i, i + 300); let score = 0; // Count how many query words appear in this section words.forEach(word => { if (word && section.includes(word)) score++; }); if (score > bestScore) { bestScore = score; bestStart = i; } } // Extract the best section and clean it up let preview = content.substring(bestStart, bestStart + 200); // Try to start at sentence beginning const sentenceStart = preview.indexOf('. '); if (sentenceStart > 0 && sentenceStart < 50) { preview = preview.substring(sentenceStart + 2); } // Clean up and add ellipsis if needed preview = preview.replace(/\n+/g, ' ').trim(); if (bestStart > 0) preview = '...' + preview; if (bestStart + 200 < content.length) preview = preview + '...'; return preview; }; // Format results const formattedResults = results.map((doc, index) => { const preview = params.searchType === 'text' || params.searchType === 'hybrid' ? getSmartPreview(doc.content, params.query) : doc.content.substring(0, 200).replace(/\n/g, ' '); const scoreInfo = params.searchType === 'hybrid' && doc.vectorScore && doc.textScore ? ` (vector: ${doc.vectorScore.toFixed(3)}, text: ${doc.textScore.toFixed(3)})` : ''; // Ensure score is always a number before calling toFixed const scoreValue = typeof doc.score === 'number' ? doc.score : 0; // Add content stats for AI context awareness const contentStats = `[${(doc.content.length / 1024).toFixed(1)}KB, ${doc.metadata.version || 1} versions]`; return `## ${index + 1}. ${doc.fileName} (${doc.metadata.type}) ${contentStats} Score: ${scoreValue.toFixed(3)}${scoreInfo} Preview: ${preview}`; }).join('\n\n'); // Create search type explanation let searchExplanation = ''; if (params.searchType === 'hybrid') { searchExplanation = `๐Ÿ’Ž MongoDB $rankFusion Hybrid Search - The CROWN JEWEL! ๐Ÿงช Algorithm: 70% Semantic (Voyage AI) + 30% Keyword (Atlas Search) ๐Ÿ”„ Reciprocal Rank Fusion intelligently combined both approaches! This search understood BOTH: - What you meant conceptually (semantic vectors) - The exact words you used (text matching)`; } else if (params.searchType === 'vector') { searchExplanation = `๐Ÿง  Vector Search: Found semantically similar content using Voyage AI embeddings ๐Ÿ’ก TIP: Use 'hybrid' search for even better results!`; } else { searchExplanation = `๐Ÿ“ Text Search: Found keyword matches using Atlas Search ๐Ÿ’ก TIP: Use 'hybrid' search to also find conceptually related content!`; } return { content: [ { type: 'text', text: `๐Ÿ” MongoDB Search Results for: "${params.query}" ${searchExplanation} ๐Ÿ“Š Found ${results.length} matches: ${formattedResults} ๐Ÿ“ NEXT STEPS: 1. ๐Ÿ“– Read the most relevant file: memory_bank/read --fileName "[filename]" 2. ๐Ÿ”„ Update your knowledge: memory_bank/update --fileName "[filename]" 3. ๐Ÿš€ Create new features based on patterns found! ๐Ÿ† MongoDB ADVANTAGE: ONE database for: - Operational data โœ“ - Vector embeddings โœ“ - Full-text search โœ“ - Version history โœ“ No external vector DB needed - MongoDB does it ALL!`, }, ], }; } catch (error) { logger.error('Search tool error:', error); // Provide user-friendly error messages let errorMessage = 'Search failed: '; const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.includes('no mongodb atlas search index found')) { errorMessage = `Search indexes are being created. Please wait a moment and try again. The following indexes are being set up: - Vector search index for semantic search - Atlas Search index for text search This typically takes 1-2 minutes. Run memory_bank/sync to ensure indexes are created.`; } else if (errorMsg.includes('text index required')) { errorMessage = `Text search is not available yet. The Atlas Search index is being created. In the meantime, you can use: - searchType: "vector" for semantic search - Run memory_bank/sync to create search indexes`; } else if (error instanceof Error && 'code' in error && error.code === 6) { errorMessage = 'Connection error. Please check your MongoDB connection and try again.'; } else { errorMessage += errorMsg || 'Unknown error occurred'; } return { content: [ { type: 'text', text: errorMessage, }, ], }; } } //# sourceMappingURL=search.js.map