Skip to main content
Glama
apolosan

Design Patterns MCP Server

by apolosan
pattern-matcher.ts23.8 kB
/** * Pattern Matcher Service for Design Patterns MCP Server * Matches user queries to appropriate design patterns using multiple strategies */ import { DatabaseManager } from './database-manager.js'; import { VectorOperationsService } from './vector-operations.js'; import { PatternRecommendation } from '../models/recommendation.js'; import { PatternAnalyzer } from './pattern-analyzer.js'; import { CacheService } from './cache.js'; import { structuredLogger } from '../utils/logger.js'; import { parseTags, parseArrayProperty } from '../utils/parse-tags.js'; import { EmbeddingServiceAdapter } from '../adapters/embedding-service-adapter.js'; // Define CodeAnalysisResult interface locally since it's not exported in compiled JS interface CodeAnalysisResult { identifiedPatterns: { pattern: string; category: string; confidence: number; location?: { line?: number; column?: number; snippet?: string; }; indicators: string[]; }[]; suggestedPatterns: { pattern: string; reason: string; confidence: number; }[]; improvements: string[]; antiPatterns?: { pattern: string; reason: string; severity: 'low' | 'medium' | 'high'; }[]; } interface PatternMatcherConfig { maxResults: number; minConfidence: number; useSemanticSearch: boolean; useKeywordSearch: boolean; useHybridSearch: boolean; semanticWeight: number; keywordWeight: number; } // Local interfaces for pattern matching interface PatternSummary { id: string; name: string; category: string; description: string; complexity?: string; tags?: string[]; } interface PatternRequest { id: string; query: string; categories?: string[]; maxResults?: number; programmingLanguage?: string; } interface MatchResult { pattern: PatternSummary; confidence: number; matchType: 'semantic' | 'keyword' | 'hybrid'; reasons: string[]; metadata: { semanticScore?: number; keywordScore?: number; finalScore: number; }; } export class PatternMatcher { private db: DatabaseManager; private vectorOps: VectorOperationsService; private config: PatternMatcherConfig; private patternAnalyzer: PatternAnalyzer; private embeddingAdapter: EmbeddingServiceAdapter | null = null; private cache: CacheService; constructor( db: DatabaseManager, vectorOps: VectorOperationsService, config: PatternMatcherConfig, cache?: CacheService ) { this.db = db; this.vectorOps = vectorOps; this.config = config; this.patternAnalyzer = new PatternAnalyzer(); this.cache = cache || new CacheService(); // Embedding adapter will be initialized lazily in generateQueryEmbedding } /** * Find patterns matching a user request */ async findMatchingPatterns(request: PatternRequest): Promise<PatternRecommendation[]> { try { // Check cache first const cacheKey = `pattern_match:${request.query}:${JSON.stringify({ categories: request.categories?.sort(), maxResults: request.maxResults, programmingLanguage: request.programmingLanguage, })}`; const cachedResult = this.cache.get(cacheKey); if (cachedResult) { return cachedResult; } const matches = await this.performMatching(request); const recommendations = await this.buildRecommendations(matches, request); // Sort by confidence and limit results recommendations.sort((a, b) => b.confidence - a.confidence); const finalResults = recommendations.slice(0, request.maxResults || this.config.maxResults); // Cache the results for 30 minutes this.cache.set(cacheKey, finalResults, 1800000); return finalResults; } catch (error) { structuredLogger.error('pattern-matcher', 'Pattern matching failed', error as Error); throw error; } } /** * Perform pattern matching using configured strategies */ private async performMatching(request: PatternRequest): Promise<MatchResult[]> { const allMatches: MatchResult[] = []; // Semantic search if (this.config.useSemanticSearch) { const semanticMatches = await this.semanticSearch(request); allMatches.push(...semanticMatches); } // Keyword search if (this.config.useKeywordSearch) { const keywordMatches = await this.keywordSearch(request); allMatches.push(...keywordMatches); } // If no matches found, try a broader keyword search if (allMatches.length === 0) { structuredLogger.warn('pattern-matcher', 'No matches found, trying broader search'); const broadMatches = await this.broadKeywordSearch(request); allMatches.push(...broadMatches); } // Hybrid search (combine results) if (this.config.useHybridSearch && allMatches.length > 0) { return this.combineMatches(allMatches); } return allMatches; } /** * Perform semantic search using vector similarity */ private async semanticSearch(request: PatternRequest): Promise<MatchResult[]> { try { // Generate embedding for the query const queryEmbedding = await this.generateQueryEmbedding(request.query); // Search for similar patterns const searchResults = await this.vectorOps.searchSimilar(queryEmbedding, { categories: request.categories, minUsageCount: 0, }); const matches = searchResults.map(result => ({ pattern: { id: result.patternId, name: result.pattern?.name || 'Unknown Pattern', category: result.pattern?.category || 'Unknown', description: result.pattern?.description || 'No description available', }, confidence: result.score, matchType: 'semantic' as const, reasons: [`Semantic similarity: ${(result.score * 100).toFixed(1)}%`], metadata: { semanticScore: result.score, finalScore: result.score, }, })); return matches; } catch (error) { structuredLogger.error( 'pattern-matcher', 'Semantic search failed, falling back to keyword search', error as Error ); // Fallback to keyword search if semantic fails return this.keywordSearch(request); } } /** * Perform keyword-based search */ private async keywordSearch(request: PatternRequest): Promise<MatchResult[]> { try { const queryWords = this.tokenizeQuery(request.query); const matches: MatchResult[] = []; // Build SQL query let sql = ` SELECT id, name, category, description, complexity, tags FROM patterns `; const params: any[] = []; if (request.categories && request.categories.length > 0) { sql += ` WHERE category IN (${request.categories.map(() => '?').join(',')})`; params.push(...request.categories); } const patterns = this.db.query<{ id: string; name: string; category: string; description: string; complexity: string; tags: string; }>(sql, params); for (const pattern of patterns) { const score = this.calculateKeywordScore(queryWords, pattern); const confidence = Math.min(score / 10, 0.99); // Normalize score, cap at 0.99 if (confidence >= this.config.minConfidence) { matches.push({ pattern: { id: pattern.id, name: pattern.name, category: pattern.category, description: pattern.description, complexity: pattern.complexity, tags: parseTags(pattern.tags), }, confidence, matchType: 'keyword' as const, reasons: this.generateKeywordReasons(queryWords, pattern), metadata: { keywordScore: score, finalScore: confidence, }, }); } } return matches; } catch (error) { structuredLogger.error('pattern-matcher', 'Keyword search failed', error as Error); return []; } } /** * Perform broad keyword search (lower threshold, no category filter) */ private async broadKeywordSearch(request: PatternRequest): Promise<MatchResult[]> { try { const queryWords = this.tokenizeQuery(request.query); const matches: MatchResult[] = []; // Get all patterns (no category filter) const patterns = this.db.query<{ id: string; name: string; category: string; description: string; complexity: string; tags: string; }>(`SELECT id, name, category, description, complexity, tags FROM patterns`); for (const pattern of patterns) { const score = this.calculateKeywordScore(queryWords, pattern); const confidence = Math.min(score / 10, 0.99); // Lower threshold for broad search if (confidence >= 0.01) { matches.push({ pattern: { id: pattern.id, name: pattern.name, category: pattern.category, description: pattern.description, complexity: pattern.complexity, tags: parseTags(pattern.tags), }, confidence, matchType: 'keyword' as const, reasons: this.generateKeywordReasons(queryWords, pattern), metadata: { keywordScore: score, finalScore: confidence, }, }); } } // Return top matches return matches.sort((a, b) => b.confidence - a.confidence).slice(0, 10); } catch (error) { structuredLogger.error('pattern-matcher', 'Broad keyword search failed', error as Error); return []; } } /** * Combine semantic and keyword matches using hybrid scoring */ private combineMatches(matches: MatchResult[]): MatchResult[] { // Create a map of patterns for O(1) lookup const patternsById = new Map<string, any>(); // Note: We need to get all patterns to build this map // For now, we'll use a simplified approach const patternMap = new Map<string, MatchResult[]>(); // Group matches by pattern for (const match of matches) { const existing = patternMap.get(match.pattern.id) || []; existing.push(match); patternMap.set(match.pattern.id, existing); } // Combine scores for each pattern const combinedMatches: MatchResult[] = []; for (const [, patternMatches] of patternMap) { const semanticMatch = patternMatches.find(m => m.matchType === 'semantic'); const keywordMatch = patternMatches.find(m => m.matchType === 'keyword'); const semanticScore = semanticMatch?.metadata.semanticScore || 0; const keywordScore = keywordMatch?.metadata.keywordScore || 0; // Improved weighted scoring - avoid division by zero and handle cases where one score is missing let finalScore = 0; if (semanticScore > 0 && keywordScore > 0) { // Both scores available - use weighted average finalScore = (this.config.semanticWeight * semanticScore + this.config.keywordWeight * keywordScore) / (this.config.semanticWeight + this.config.keywordWeight); } else if (semanticScore > 0) { // Only semantic score - use it directly finalScore = semanticScore; } else if (keywordScore > 0) { // Only keyword score - use it directly finalScore = keywordScore; } // Ensure final score is normalized between 0 and 1 finalScore = Math.min(Math.max(finalScore, 0), 1); const reasons = [...(semanticMatch?.reasons || []), ...(keywordMatch?.reasons || [])]; combinedMatches.push({ pattern: patternMatches[0].pattern, confidence: finalScore, matchType: 'hybrid' as const, reasons, metadata: { semanticScore, keywordScore, finalScore, }, }); } return combinedMatches; } /** * Build pattern recommendations from matches */ private async buildRecommendations( matches: MatchResult[], request: PatternRequest ): Promise<PatternRecommendation[]> { const recommendations: PatternRecommendation[] = []; for (let i = 0; i < matches.length; i++) { const match = matches[i]; const pattern = await this.getDetailedPattern(match.pattern.id); if (pattern) { const recommendation: PatternRecommendation = { id: crypto.randomUUID(), requestId: request.id, pattern: { id: pattern.id, name: pattern.name, category: pattern.category, description: pattern.description, complexity: pattern.complexity, tags: pattern.tags, }, confidence: match.confidence, rank: i + 1, justification: { primaryReason: match.reasons[0] || 'Pattern matches query requirements', supportingReasons: match.reasons.slice(1), problemFit: this.generateProblemFit(match, request), benefits: pattern.benefits || [], drawbacks: pattern.drawbacks || [], }, implementation: await this.generateImplementationGuidance(pattern, request), alternatives: await this.findAlternatives(pattern.id, matches), context: { projectContext: this.extractProjectContext(request), teamContext: this.extractTeamContext(request), technologyFit: { fitScore: 0.8, // Simplified reasons: ['Good fit for the specified programming language'], compatibleTech: [request.programmingLanguage || 'typescript'], incompatibleTech: [], integrationRequirements: [], }, }, }; recommendations.push(recommendation); } } return recommendations; } /** * Generate query embedding using the same strategy as pattern embeddings */ private async generateQueryEmbedding(query: string): Promise<number[]> { try { // Ensure adapter is initialized if (!this.embeddingAdapter) { this.embeddingAdapter = new EmbeddingServiceAdapter({ cacheEnabled: true, cacheTTL: 3600000, // 1 hour batchSize: 10, retryAttempts: 3, retryDelay: 1000, preferredStrategy: 'transformers', fallbackToSimple: true, }); } // Initialize if needed if (!(await this.embeddingAdapter.isReady())) { await this.embeddingAdapter.initialize(); } // Use the embedding adapter to generate query embedding with the same strategy const embedding = await this.embeddingAdapter.generateEmbedding(query); if (!embedding || embedding.length === 0) { throw new Error('Empty embedding generated'); } return embedding; } catch (error) { structuredLogger.error( 'pattern-matcher', 'Failed to generate query embedding, using fallback', error as Error ); // Fallback to simple hash-based embedding if the adapter fails return this.generateFallbackEmbedding(query); } } /** * Fallback embedding generation using simple hash (legacy behavior) */ private generateFallbackEmbedding(query: string): number[] { const words = query.toLowerCase().split(/\s+/); const embedding = new Array(384).fill(0); // Match all-MiniLM-L6-v2 dimensions // Create a simple hash-based embedding for (let i = 0; i < words.length; i++) { const word = words[i]; const wordHash = this.simpleHash(word); for (let j = 0; j < Math.min(word.length, 10); j++) { const charCode = word.charCodeAt(j); const position = (wordHash + j + i * 7) % embedding.length; // Improved algorithm to avoid zeros embedding[position] += (charCode / 255) * 0.5 + Math.sin(wordHash * j) * 0.3; } } // Normalize the embedding const norm = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0)); const normalizedEmbedding = embedding.map(val => val / (norm || 1)); // Cache the embedding for 1 hour this.cache.setEmbeddings(query, normalizedEmbedding, 3600000); return normalizedEmbedding; } /** * Simple hash function (same as generate-embeddings.ts) */ private simpleHash(text: string): number { let hash = 0; for (let i = 0; i < text.length; i++) { const char = text.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash); } /** * Tokenize query for keyword search */ private tokenizeQuery(query: string): string[] { return query .toLowerCase() .replace(/[^\w\s]/g, ' ') .split(/\s+/) .filter(word => word.length > 2); } /** * Calculate keyword matching score */ private calculateKeywordScore(queryWords: string[], pattern: any): number { let score = 0; const patternText = `${pattern.name} ${pattern.description} ${pattern.tags}`.toLowerCase(); for (const word of queryWords) { if (patternText.includes(word)) { score += 0.5; // Reduced from 1 } // Bonus for exact matches in name if (pattern.name.toLowerCase().includes(word)) { score += 1; // Reduced from 2 } // Bonus for category matches if (pattern.category.toLowerCase().includes(word)) { score += 0.5; // Reduced from 1.5 } } return score; } /** * Generate reasons for keyword matches */ private generateKeywordReasons(queryWords: string[], pattern: any): string[] { const reasons: string[] = []; for (const word of queryWords) { if (pattern.name.toLowerCase().includes(word)) { reasons.push(`Pattern name contains "${word}"`); } if (pattern.description.toLowerCase().includes(word)) { reasons.push(`Pattern description mentions "${word}"`); } if (pattern.category.toLowerCase().includes(word)) { reasons.push(`Pattern category matches "${word}"`); } } return reasons.length > 0 ? reasons : ['Keyword-based pattern match']; } /** * Generate problem-solution fit explanation */ private generateProblemFit(match: MatchResult, request: PatternRequest): string { return `This pattern addresses your requirement for "${request.query}" by providing a proven solution for ${match.pattern.category.toLowerCase()} scenarios.`; } /** * Generate implementation guidance */ private async generateImplementationGuidance( pattern: any, request: PatternRequest ): Promise<any> { const implementations = await this.getPatternImplementations( pattern.id, request.programmingLanguage ); return { steps: [ 'Analyze your current code structure', 'Identify where the pattern applies', 'Implement the pattern following the examples', 'Test the implementation', 'Refactor as needed', ], examples: implementations.map((impl: any) => ({ language: impl.language, title: `${pattern.name} in ${impl.language}`, code: impl.code, explanation: impl.explanation, })), dependencies: [], configuration: [], testing: { unitTests: ['Test pattern implementation', 'Test edge cases'], integrationTests: ['Test pattern interaction with existing code'], testScenarios: ['Normal operation', 'Error conditions', 'Boundary cases'], }, performance: { impact: 'medium', memoryUsage: 'Minimal additional memory', cpuUsage: 'Negligible CPU overhead', optimizations: ['Consider lazy initialization', 'Use appropriate caching'], monitoring: ['Monitor pattern usage', 'Track performance metrics'], }, }; } /** * Find alternative patterns */ private async findAlternatives(patternId: string, allMatches: MatchResult[]): Promise<any[]> { // Get related patterns from database const relatedPatterns = this.db.query<{ target_pattern_id: string; type: string; description: string; }>( ` SELECT target_pattern_id, type, description FROM pattern_relationships WHERE source_pattern_id = ? AND type IN ('alternative', 'similar') `, [patternId] ); return relatedPatterns.slice(0, 3).map(rel => ({ pattern: allMatches.find(m => m.pattern.id === rel.target_pattern_id)?.pattern || { id: rel.target_pattern_id, name: 'Unknown Pattern', category: 'Unknown', description: 'Pattern information not available', complexity: 'Unknown', tags: [], }, reasonNotChosen: rel.description, whenToChoose: `Consider when you need ${rel.type} approach`, tradeoffs: [`Different ${rel.type} characteristics`], })); } /** * Get detailed pattern information */ private async getDetailedPattern(patternId: string): Promise<any | null> { const pattern = this.db.queryOne( ` SELECT id, name, category, description, when_to_use, benefits, drawbacks, use_cases, complexity, tags, created_at, updated_at FROM patterns WHERE id = ? `, [patternId] ); if (!pattern) return null; return { ...pattern, when_to_use: parseArrayProperty(pattern.when_to_use, 'when_to_use'), benefits: parseArrayProperty(pattern.benefits, 'benefits'), drawbacks: parseArrayProperty(pattern.drawbacks, 'drawbacks'), use_cases: parseArrayProperty(pattern.use_cases, 'use_cases'), tags: parseTags(pattern.tags), }; } /** * Get pattern implementations */ private async getPatternImplementations(patternId: string, language?: string): Promise<any[]> { let sql = 'SELECT id, language, code, explanation FROM pattern_implementations WHERE pattern_id = ?'; const params: any[] = [patternId]; if (language) { sql += ' AND language = ?'; params.push(language); } sql += ' ORDER BY language, created_at DESC'; const implementations = this.db.query(sql, params); return implementations.slice(0, 3); // Return top 3 implementations } /** * Extract project context from request */ private extractProjectContext(_request: PatternRequest): any { // Simplified context extraction return { size: 'medium', maturity: 'established', existingPatterns: [], constraints: [], }; } /** * Extract team context from request */ private extractTeamContext(_request: PatternRequest): any { // Simplified context extraction return { size: 'medium', experience: 'intermediate', learningPreferences: ['examples', 'documentation'], }; } /** * Analyze code to detect patterns and suggest improvements */ async analyzeCode(code: string, language: string): Promise<CodeAnalysisResult> { return await this.patternAnalyzer.analyzeCode(code, language); } } // Default configuration const DEFAULT_PATTERN_MATCHER_CONFIG: PatternMatcherConfig = { maxResults: 5, minConfidence: 0.3, useSemanticSearch: true, useKeywordSearch: true, useHybridSearch: true, semanticWeight: 0.7, keywordWeight: 0.3, }; // Factory function export function createPatternMatcher( db: DatabaseManager, vectorOps: VectorOperationsService, config?: Partial<PatternMatcherConfig> ): PatternMatcher { const finalConfig = { ...DEFAULT_PATTERN_MATCHER_CONFIG, ...config }; return new PatternMatcher(db, vectorOps, finalConfig); }

Implementation Reference

Latest Blog Posts

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/apolosan/design_patterns_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server