UNPKG

mongodocs-mcp

Version:

Lightning-fast semantic search for MongoDB documentation via Model Context Protocol. 10,000+ documents, <500ms search.

497 lines (493 loc) • 19.8 kB
#!/usr/bin/env node /** * MongoDB Semantic Docs MCP Server v7.2.2 * Lightning-fast semantic search for MongoDB documentation */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import dotenv from 'dotenv'; import { HybridSearchEngine } from './core/hybrid-search-engine.js'; import { DocumentRefresher } from './core/document-refresher.js'; // Load environment variables dotenv.config(); class MongoDBSemanticMCP { server; searchEngine; refresher; constructor() { this.server = new Server({ name: 'mongodocs-mcp', version: '5.0.0', }, { capabilities: { tools: { listChanged: true }, }, }); this.searchEngine = new HybridSearchEngine(); this.refresher = new DocumentRefresher(); this.registerTools(); } registerTools() { // Register available MCP tools (search operations only) this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'mongodb-semantic-search', description: 'Search MongoDB and Voyage AI documentation using natural language semantic search', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language question or search query', }, products: { type: 'array', items: { type: 'string' }, description: 'Filter by MongoDB products or Voyage AI docs', }, version: { type: 'string', description: 'MongoDB version (e.g., "7.0", "8.0")', }, limit: { type: 'number', description: 'Number of results to return', default: 5, minimum: 1, maximum: 20, }, includeCode: { type: 'boolean', description: 'Include code examples in results', }, }, required: ['query'], }, }, { name: 'mongodb-find-similar', description: 'Find MongoDB and Voyage AI documentation similar to provided content', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Reference content to find similar documents', }, limit: { type: 'number', description: 'Number of results to return', default: 5, minimum: 1, maximum: 10, }, }, required: ['content'], }, }, { name: 'mongodb-explain-concept', description: 'Get comprehensive explanation of a MongoDB or Voyage AI concept', inputSchema: { type: 'object', properties: { concept: { type: 'string', description: 'MongoDB or Voyage AI concept to explain', }, depth: { type: 'string', description: 'Level of detail', enum: ['beginner', 'intermediate', 'advanced'], default: 'intermediate', }, }, required: ['concept'], }, }, { name: 'mongodb-refresh-docs', description: 'Update MongoDB and Voyage AI documentation database', inputSchema: { type: 'object', properties: { mode: { type: 'string', description: 'Refresh mode', enum: ['incremental', 'full'], default: 'incremental', }, products: { type: 'array', items: { type: 'string' }, description: 'Products to refresh (including Voyage AI)', }, }, }, }, { name: 'mongodb-status', description: 'Get system status and statistics', inputSchema: { type: 'object', properties: {}, }, }, ], })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'mongodb-semantic-search': return await this.handleSemanticSearch(args); case 'mongodb-find-similar': return await this.handleFindSimilar(args); case 'mongodb-explain-concept': return await this.handleExplainConcept(args); case 'mongodb-refresh-docs': return await this.handleRefreshDocs(args); case 'mongodb-status': return await this.handleStatus(); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { console.error(`Error in tool ${name}:`, error); return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`, }, ], }; } }); } async handleSemanticSearch(args) { try { console.error(`šŸ” Searching for: "${args.query}"`); const filter = this.buildFilter(args); const results = await this.searchEngine.search(args.query, { limit: args.limit || 5, filter, includeCode: args.includeCode, }); console.error(`āœ… Found ${results.length} results`); // Always return proper response, even if no results if (!results || results.length === 0) { return { content: [ { type: 'text', text: 'No results found. Try refining your search query.', }, ], }; } return { content: [ { type: 'text', text: this.formatSearchResults(results), }, ], }; } catch (error) { console.error('āŒ Search error:', error); return { content: [ { type: 'text', text: `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], }; } } async handleFindSimilar(args) { try { const results = await this.searchEngine.findSimilar(args.content, args.limit || 5); return { content: [ { type: 'text', text: this.formatSimilarResults(results), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Find similar failed: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], }; } } async handleExplainConcept(args) { try { const explanation = await this.searchEngine.explainConcept(args.concept, args.depth || 'intermediate'); return { content: [ { type: 'text', text: explanation, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Explain concept failed: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], }; } } async handleRefreshDocs(args) { try { const result = await this.refresher.refresh({ mode: args.mode || 'incremental', products: args.products, }); return { content: [ { type: 'text', text: `Refresh completed: ${result.documentsUpdated || 0} documents updated.`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], }; } } async handleStatus() { try { const status = await this.searchEngine.getStatus(); return { content: [ { type: 'text', text: this.formatStatus(status), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Status check failed: ${error instanceof Error ? error.message : 'Unknown error'}\nPlease run the indexer first.`, }, ], }; } } buildFilter(args) { const filter = {}; if (args.products && args.products.length > 0) { filter['metadata.product'] = { $in: args.products }; } if (args.version) { filter['metadata.version'] = args.version; } if (args.includeCode) { filter['metadata.hasCode'] = true; } return Object.keys(filter).length > 0 ? filter : undefined; } formatSearchResults(results) { if (results.length === 0) { return 'No results found. Try refining your search query.'; } let output = `Found ${results.length} relevant documents:\n\n`; results.forEach((result, index) => { // Extract product and title from documentId (format: product_version_hash) const parts = result.documentId?.split('_') || []; const product = parts[0] || 'unknown'; const title = `Document ${result.documentId || index + 1}`; const score = result.maxScore || result.score || 0; output += `## ${index + 1}. ${title}\n`; output += `**Product**: ${product} | **Score**: ${score.toFixed(3)} | **Source**: ${result.source || 'vector'}\n`; output += `**Document ID**: ${result.documentId}\n\n`; // Show content from chunks if (result.chunks && result.chunks.length > 0) { const topChunks = result.chunks.slice(0, 2); topChunks.forEach((chunk, chunkIndex) => { output += `**Chunk ${chunkIndex + 1}** (Score: ${chunk.score?.toFixed(3) || 'N/A'}):\n`; output += `${chunk.content}\n\n`; }); } else if (result.content) { output += `${result.content.substring(0, 300)}...\n\n`; } output += '---\n\n'; }); return output; } formatSimilarResults(results) { if (results.length === 0) { return 'No similar documents found.'; } let output = `Found ${results.length} similar documents:\n\n`; results.forEach((result, index) => { const title = result.metadata?.title || result.title || 'Document'; const url = result.metadata?.url || result.url || '#'; const score = result.score || 0; output += `${index + 1}. **${title}** (Score: ${score.toFixed(3)})\n`; if (result.content) { output += ` ${result.content.substring(0, 150)}...\n`; } output += ` ${url}\n\n`; }); return output; } formatStatus(status) { return `**MongoDB Semantic Docs MCP Status** šŸ“Š **Database Statistics:** - Total Documents: ${status.totalDocuments} - Total Chunks: ${status.totalChunks} - Index Status: ${status.indexStatus} - Last Update: ${status.lastUpdate} šŸ” **Search Performance:** - Average Query Time: ${status.avgQueryTime}ms - Total Searches Today: ${status.searchesToday} šŸ’¾ **Storage:** - Database Size: ${status.dbSize} - Vector Index Size: ${status.indexSize} āœ… **System Health:** ${status.health}`; } async start() { // Check if environment is configured if (!process.env.MONGODB_URI || !process.env.VOYAGE_API_KEY) { console.error('Missing required environment variables.'); console.error('Please set MONGODB_URI and VOYAGE_API_KEY'); console.error('Or run: mongodocs-index --setup'); process.exit(1); } // Initialize connections - CRITICAL: Must succeed for MCP to work! try { console.error('Initializing search engine...'); await this.searchEngine.initialize(); console.error('āœ… Search engine initialized'); // CRITICAL FIX: Verify vector search index is ready console.error('Verifying vector search index...'); await this.verifyVectorSearchIndex(); console.error('āœ… Vector search index verified'); // CRITICAL FIX: Test search functionality console.error('Testing search functionality...'); await this.testSearchFunctionality(); console.error('āœ… Search functionality verified'); console.error('Initializing document refresher...'); await this.refresher.initialize(); console.error('āœ… Document refresher initialized'); } catch (error) { console.error('āŒ FATAL: Failed to initialize MongoDB connection:', error); console.error('Please check:'); console.error('1. MONGODB_URI environment variable is set'); console.error('2. VOYAGE_API_KEY environment variable is set'); console.error('3. MongoDB Atlas is accessible'); console.error('4. Network connection is working'); console.error('5. Vector search index is ready'); console.error('6. Database contains indexed documents'); throw error; // FAIL FAST - Don't start broken MCP! } // Start the MCP server const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('MongoDB Semantic Docs MCP Server v7.2.2 started'); console.error('Ready for lightning-fast searches!'); console.error(`Connected to: ${process.env.MONGODB_URI?.split('@')[1]?.split('/')[0] || 'MongoDB'}`); // Get actual document count const status = await this.searchEngine.getStatus(); console.error(`Documents indexed: ${status.totalDocuments || 0}`); } /** * CRITICAL FIX: Verify vector search index is ready */ async verifyVectorSearchIndex() { const mongodb = this.searchEngine['mongodb']; // Access private member const collection = mongodb.getVectorsCollection(); // Check if index exists and is ready const indexes = await collection.listSearchIndexes().toArray(); const vectorIndex = indexes.find((i) => i.name === 'semantic_search'); if (!vectorIndex) { throw new Error('Vector search index "semantic_search" not found. Please run the indexer first.'); } if (vectorIndex.status !== 'READY') { throw new Error(`Vector search index is not ready. Status: ${vectorIndex.status}. Please wait for index to build.`); } } /** * CRITICAL FIX: Test search functionality with a simple query */ async testSearchFunctionality() { try { // Test with a simple MongoDB query const testResults = await this.searchEngine.search('mongodb find', { limit: 1 }); if (!Array.isArray(testResults)) { throw new Error('Search returned invalid results format'); } // Check if we have any documents at all const mongodb = this.searchEngine['mongodb']; const collection = mongodb.getVectorsCollection(); const docCount = await collection.countDocuments(); if (docCount === 0) { throw new Error('No documents found in database. Please run the indexer first.'); } console.error(`Database contains ${docCount} documents`); } catch (error) { throw new Error(`Search functionality test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async stop() { // Cleanup connections - methods will be implemented in engines // await this.searchEngine.close(); // await this.refresher.close(); } } // Run the server const server = new MongoDBSemanticMCP(); // Handle graceful shutdown process.on('SIGINT', async () => { await server.stop(); process.exit(0); }); process.on('SIGTERM', async () => { await server.stop(); process.exit(0); }); // Start the server server.start().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); export default MongoDBSemanticMCP; //# sourceMappingURL=index.js.map