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
JavaScript
/**
* 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();