UNPKG

mcp-orchestrator

Version:

MCP Orchestrator - Discover and install MCPs with automatic OAuth support. Uses Claude CLI for OAuth MCPs (Canva, Asana, etc). 34 trusted MCPs from Claude Partners.

216 lines (215 loc) 7.67 kB
/** * Semantic Search Engine * Fast, local vector search for MCP discovery */ import * as fs from 'fs'; import * as path from 'path'; export class SemanticSearchEngine { indexPath; index = null; isLoaded = false; constructor(indexPath) { this.indexPath = indexPath; if (!indexPath) { this.indexPath = path.join(process.cwd(), 'data', 'mcp-index.min.json'); } // Auto-load on construction this.loadSync(); } /** * Load the index into memory (synchronous) */ loadSync() { if (this.isLoaded) return; try { const indexData = fs.readFileSync(this.indexPath, 'utf-8'); this.index = JSON.parse(indexData); this.isLoaded = true; console.error(`📚 Loaded ${this.index.totalMCPs} MCPs into search engine`); } catch (error) { console.error('Failed to load index:', error); throw new Error('Search index not found. Run npm run build:index first.'); } } /** * Search for MCPs that match the query (synchronous) */ search(query, limit = 5) { if (!this.isLoaded) { this.loadSync(); } // Generate query embedding const queryEmbedding = this.generateQueryEmbedding(query); // Score all MCPs const scores = this.index.mcps.map((mcp) => { // Calculate cosine similarity const similarity = this.cosineSimilarity(queryEmbedding, mcp.embedding); // Boost score based on keyword matches const keywordBoost = this.calculateKeywordBoost(query, mcp); // Combine scores const finalScore = similarity * 0.7 + keywordBoost * 0.3; return { mcp, score: finalScore, reason: this.generateReason(mcp, query, similarity, keywordBoost) }; }); // Sort by score and return top results return scores .sort((a, b) => b.score - a.score) .slice(0, limit) .filter((result) => result.score > 0.1); } /** * Find MCPs by explicit need */ async findByNeed(need) { // Map common needs to specific searches const needMap = { 'file': 'read write files filesystem csv json', 'database': 'sql query database sqlite postgres data', 'web': 'web scrape browser fetch http api', 'git': 'git repository commit version control', 'slack': 'slack message chat notification', 'search': 'search web find query brave google' }; // Enhance the need with related terms const enhancedQuery = needMap[need.toLowerCase()] || need; return this.search(enhancedQuery); } /** * Generate embedding for a query */ generateQueryEmbedding(query) { const keywords = [ 'file', 'read', 'write', 'directory', 'folder', 'csv', 'json', 'text', 'database', 'sql', 'query', 'data', 'table', 'sqlite', 'postgres', 'web', 'http', 'api', 'fetch', 'scrape', 'browser', 'html', 'git', 'github', 'commit', 'repository', 'branch', 'merge', 'slack', 'message', 'chat', 'email', 'notification', 'cloud', 'storage', 'drive', 'upload', 'download', 'search', 'find', 'query', 'lookup', 'time', 'date', 'schedule', 'calendar', 'analyze', 'process', 'transform', 'extract' ]; const embedding = new Array(384).fill(0); const lowerQuery = query.toLowerCase(); // Set values based on keyword presence keywords.forEach((keyword, index) => { if (lowerQuery.includes(keyword)) { const position = (index * 13) % 384; embedding[position] = 1; if (position > 0) embedding[position - 1] = 0.5; if (position < 383) embedding[position + 1] = 0.5; } }); // Normalize const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0)); if (magnitude > 0) { return embedding.map(val => val / magnitude); } return embedding; } /** * Calculate cosine similarity between two vectors */ cosineSimilarity(a, b) { if (!a || !b || a.length !== b.length) return 0; let dotProduct = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; } return dotProduct; } /** * Calculate keyword boost */ calculateKeywordBoost(query, mcp) { const queryWords = query.toLowerCase().split(/\W+/); let boost = 0; // Check name queryWords.forEach(word => { if (mcp.name.toLowerCase().includes(word)) { boost += 0.3; } }); // Check keywords if (mcp.keywords) { queryWords.forEach(word => { if (mcp.keywords.some((k) => k.toLowerCase().includes(word))) { boost += 0.2; } }); } // Check description queryWords.forEach(word => { if (mcp.description.toLowerCase().includes(word)) { boost += 0.1; } }); // Check use cases if (mcp.useCases) { queryWords.forEach(word => { if (mcp.useCases.some((uc) => uc.toLowerCase().includes(word))) { boost += 0.15; } }); } return Math.min(boost, 1); // Cap at 1 } /** * Generate human-readable reason for match */ generateReason(mcp, query, similarity, keywordBoost) { const reasons = []; const queryWords = query.toLowerCase().split(/\W+/); // Check for keyword matches const matchedKeywords = mcp.keywords.filter((k) => queryWords.some(w => k.toLowerCase().includes(w))); if (matchedKeywords.length > 0) { reasons.push(`Matches keywords: ${matchedKeywords.join(', ')}`); } // Check for use case matches if (mcp.useCases) { const matchedUseCases = mcp.useCases.filter((uc) => queryWords.some(w => uc.toLowerCase().includes(w))); if (matchedUseCases.length > 0) { reasons.push(`Can: ${matchedUseCases[0]}`); } } // Add semantic match info if (similarity > 0.7) { reasons.push('High semantic relevance'); } else if (similarity > 0.4) { reasons.push('Good semantic match'); } // Add capability info if (mcp.capabilities && mcp.capabilities.length > 0) { reasons.push(`Capabilities: ${mcp.capabilities.slice(0, 3).join(', ')}`); } return reasons.join('; ') || 'Related to query'; } /** * Get statistics about the index */ getStats() { if (!this.isLoaded) { return { totalMCPs: 0, loaded: false }; } return { totalMCPs: this.index.totalMCPs, loaded: true, embeddingModel: this.index.embeddingModel, sources: { official: this.index.mcps.filter((m) => m.source === 'official').length, community: this.index.mcps.filter((m) => m.source === 'community').length } }; } } // Export singleton instance export const searchEngine = new SemanticSearchEngine();