Skip to main content
Glama

mcp-adr-analysis-server

by tosin2013
memory-transformation.ts28.9 kB
/** * Memory Transformation Utilities * * Utilities for transforming existing ADRs and system data into memory entities * for the memory-centric architecture migration. */ import crypto from 'crypto'; import { ArchitecturalDecisionMemory, CodeComponentMemory, KnowledgeArtifactMemory, MemoryRelationship, MemoryEntitySchema, } from '../types/memory-entities.js'; import { DiscoveredAdr } from './adr-discovery.js'; import { MemoryEntityManager } from './memory-entity-manager.js'; import { EnhancedLogger } from './enhanced-logging.js'; import { McpAdrError } from '../types/index.js'; export class MemoryTransformer { private logger: EnhancedLogger; // @ts-ignore - keeping for future memory persistence integration private _memoryManager: MemoryEntityManager; constructor(memoryManager: MemoryEntityManager) { this.logger = new EnhancedLogger(); this._memoryManager = memoryManager; // Note: memoryManager is kept for future memory persistence integration } /** * Transform a discovered ADR into an Architectural Decision Memory entity */ async transformAdrToMemory(adr: DiscoveredAdr): Promise<ArchitecturalDecisionMemory> { try { const now = new Date().toISOString(); // Parse consequences into positive/negative/risks const consequences = this.parseConsequences(adr.consequences || ''); // Extract implementation tasks from ADR content const implementationTasks = this.extractImplementationTasks(adr.content || ''); // Determine implementation status based on ADR status const implementationStatus = this.mapAdrStatusToImplementation(adr.status); const memoryEntity: ArchitecturalDecisionMemory = { id: crypto.randomUUID(), type: 'architectural_decision', created: now, lastModified: now, version: 1, confidence: this.calculateConfidenceFromAdr(adr), relevance: this.calculateRelevanceFromAdr(adr), title: adr.title, description: adr.context || adr.title, tags: this.extractTagsFromAdr(adr), relationships: [], // Will be populated later through relationship inference context: { projectPhase: this.inferProjectPhase(adr), businessDomain: this.inferBusinessDomain(adr), technicalStack: this.inferTechnicalStack(adr), environmentalFactors: this.inferEnvironmentalFactors(adr), stakeholders: this.inferStakeholders(adr), }, accessPattern: { lastAccessed: now, accessCount: 1, accessContext: ['adr_transformation'], }, evolution: { origin: 'discovered', transformations: [ { timestamp: now, type: 'imported_from_adr', description: `Transformed from ADR: ${adr.filename}`, agent: 'MemoryTransformer', }, ], }, validation: { isVerified: adr.status === 'accepted', verificationMethod: adr.status === 'accepted' ? 'adr_status' : undefined, verificationTimestamp: adr.status === 'accepted' ? now : undefined, }, decisionData: { status: this.mapAdrStatus(adr.status), context: adr.context || '', decision: adr.decision || '', consequences: consequences, alternatives: this.extractAlternatives(adr.content || ''), implementationStatus: implementationStatus, implementationTasks: implementationTasks, reviewHistory: adr.date ? [ { timestamp: this.parseAdrDate(adr.date) || now, reviewer: 'system', decision: adr.status === 'accepted' ? 'approve' : adr.status === 'rejected' ? 'reject' : 'revise', comments: `Original ADR status: ${adr.status}`, }, ] : [], }, }; // Basic validation before schema validation if (!adr.title || adr.title.trim().length === 0) { throw new Error('ADR title is required'); } // Validate the memory entity MemoryEntitySchema.parse(memoryEntity); this.logger.debug('Transformed ADR to memory entity', 'MemoryTransformer', { adrFile: adr.filename, entityId: memoryEntity.id, status: adr.status, confidence: memoryEntity.confidence, }); return memoryEntity; } catch (error) { throw new McpAdrError( `Failed to transform ADR to memory: ${error instanceof Error ? error.message : String(error)}`, 'ADR_TRANSFORMATION_ERROR' ); } } /** * Transform multiple ADRs and infer relationships between them */ async transformAdrCollectionToMemories(adrs: DiscoveredAdr[]): Promise<{ entities: ArchitecturalDecisionMemory[]; relationships: MemoryRelationship[]; }> { const entities: ArchitecturalDecisionMemory[] = []; const relationships: MemoryRelationship[] = []; // Transform each ADR for (const adr of adrs) { try { const entity = await this.transformAdrToMemory(adr); entities.push(entity); } catch (error) { this.logger.warn('Failed to transform ADR, skipping', 'MemoryTransformer', { adrFile: adr.filename, error: error instanceof Error ? error.message : String(error), }); } } // Infer relationships between ADRs for (let i = 0; i < entities.length; i++) { for (let j = i + 1; j < entities.length; j++) { const sourceEntity = entities[i]; const targetEntity = entities[j]; if (sourceEntity && targetEntity) { const inferredRelationships = this.inferAdrRelationships(sourceEntity, targetEntity); relationships.push(...inferredRelationships); } } } this.logger.info('Transformed ADR collection to memory entities', 'MemoryTransformer', { adrCount: adrs.length, entityCount: entities.length, relationshipCount: relationships.length, }); return { entities, relationships }; } /** * Create a knowledge artifact memory from documentation or research */ async createKnowledgeArtifact( title: string, content: string, artifactType: KnowledgeArtifactMemory['artifactData']['artifactType'], sourceFile?: string ): Promise<KnowledgeArtifactMemory> { const now = new Date().toISOString(); const memoryEntity: KnowledgeArtifactMemory = { id: crypto.randomUUID(), type: 'knowledge_artifact', created: now, lastModified: now, version: 1, confidence: 0.8, relevance: 0.7, title, description: this.extractDescriptionFromContent(content), tags: this.extractTagsFromContent(content), relationships: [], context: { technicalStack: this.inferTechnicalStackFromContent(content), environmentalFactors: [], stakeholders: [], }, accessPattern: { lastAccessed: now, accessCount: 1, accessContext: ['knowledge_artifact_creation'], }, evolution: { origin: sourceFile ? 'discovered' : 'created', transformations: [ { timestamp: now, type: 'created', description: `Created knowledge artifact: ${artifactType}`, agent: 'MemoryTransformer', }, ], }, validation: { isVerified: false, }, artifactData: { artifactType, content, format: this.detectContentFormat(content), sourceReliability: sourceFile ? 0.8 : 0.6, applicabilityScope: this.inferApplicabilityScope(content), keyInsights: this.extractKeyInsights(content), actionableItems: this.extractActionableItems(content), }, }; return memoryEntity; } /** * Transform project code structure into code component memories */ async transformCodeStructureToMemories( codeStructure: any, // From project analysis projectPath: string ): Promise<CodeComponentMemory[]> { const entities: CodeComponentMemory[] = []; // This would typically analyze the project structure // For now, we'll create a placeholder implementation this.logger.info('Code structure transformation not yet implemented', 'MemoryTransformer', { projectPath, structureKeys: Object.keys(codeStructure || {}), }); return entities; } // Private helper methods private parseConsequences(consequencesText: string): { positive: string[]; negative: string[]; risks: string[]; } { const consequences = { positive: [] as string[], negative: [] as string[], risks: [] as string[], }; if (!consequencesText) return consequences; // Look for structured consequence sections const positiveMatch = consequencesText.match( /(?:positive|pros?|benefits?)[:\s]*([^]*?)(?=\n(?:negative|cons?|risks?))/is ); const negativeMatch = consequencesText.match( /(?:negative|cons?|drawbacks?)[:\s]*([^]*?)(?=\n(?:positive|risks?))/is ); const risksMatch = consequencesText.match(/(?:risks?|concerns?)[:\s]*([^]*?)$/is); // Extract positive consequences if (positiveMatch && positiveMatch[1]) { consequences.positive = this.extractListItems(positiveMatch[1]); } // Extract negative consequences if (negativeMatch && negativeMatch[1]) { consequences.negative = this.extractListItems(negativeMatch[1]); } // Extract risks if (risksMatch && risksMatch[1]) { consequences.risks = this.extractListItems(risksMatch[1]); } // If no structured format, try to infer from bullet points if (consequences.positive.length === 0 && consequences.negative.length === 0) { const lines = consequencesText .split('\n') .map(line => line.trim()) .filter(line => line); lines.forEach(line => { if (line.match(/^[-*+]\s/) || line.match(/^\d+\.\s/)) { const item = line.replace(/^[-*+]\s+|^\d+\.\s+/, '').trim(); if (item.match(/\b(benefit|advantage|positive|good|improve)/i)) { consequences.positive.push(item); } else if (item.match(/\b(drawback|disadvantage|negative|bad|risk|concern)/i)) { consequences.negative.push(item); } else { // Default to positive if unclear consequences.positive.push(item); } } }); } return consequences; } private extractListItems(text: string): string[] { if (!text) return []; return text .split('\n') .map(line => line.trim()) .filter(line => line && (line.match(/^[-*+]\s/) || line.match(/^\d+\.\s/))) .map(line => line.replace(/^[-*+]\s+|^\d+\.\s+/, '').trim()) .filter(item => item.length > 0); } private extractImplementationTasks(content: string): string[] { if (!content) return []; const tasks: string[] = []; // Look for task sections const taskSectionMatch = content.match( /(?:implementation tasks?|tasks?|todo)[:\s]*([^#]*?)(?=\n##|\n#|$)/is ); if (taskSectionMatch && taskSectionMatch[1]) { tasks.push(...this.extractListItems(taskSectionMatch[1])); } // Look for checkbox items const checkboxMatches = content.matchAll(/- \[[ x]\] (.+)/gi); for (const match of checkboxMatches) { if (match[1]) { tasks.push(match[1].trim()); } } return tasks; } private mapAdrStatusToImplementation( status: string ): ArchitecturalDecisionMemory['decisionData']['implementationStatus'] { switch (status.toLowerCase()) { case 'accepted': return 'in_progress'; case 'implemented': case 'done': case 'completed': return 'completed'; case 'proposed': case 'draft': return 'not_started'; case 'on hold': case 'deferred': return 'on_hold'; default: return 'not_started'; } } private mapAdrStatus(status: string): ArchitecturalDecisionMemory['decisionData']['status'] { switch (status.toLowerCase()) { case 'accepted': case 'approved': return 'accepted'; case 'deprecated': case 'obsolete': return 'deprecated'; case 'superseded': return 'superseded'; default: return 'proposed'; } } private calculateConfidenceFromAdr(adr: DiscoveredAdr): number { let confidence = 0.5; // Base confidence // Higher confidence for accepted ADRs if (adr.status === 'accepted') confidence += 0.3; // Higher confidence if has context and decision if (adr.context && adr.decision) confidence += 0.2; // Higher confidence if has consequences if (adr.consequences) confidence += 0.1; // Higher confidence if has date if (adr.date) confidence += 0.1; return Math.min(1.0, confidence); } private calculateRelevanceFromAdr(adr: DiscoveredAdr): number { let relevance = 0.6; // Base relevance // Higher relevance for accepted or recent ADRs if (adr.status === 'accepted') relevance += 0.2; // Lower relevance for deprecated ADRs if (adr.status === 'deprecated' || adr.status === 'superseded') { relevance -= 0.3; } // Adjust based on completeness const completeness = [adr.context, adr.decision, adr.consequences].filter(Boolean).length / 3; relevance += completeness * 0.2; return Math.min(1.0, Math.max(0.1, relevance)); } private extractTagsFromAdr(adr: DiscoveredAdr): string[] { const tags: string[] = []; // Add category as tag if available if (adr.metadata?.category) { tags.push(adr.metadata.category); } // Add existing tags if (adr.metadata?.tags) { tags.push(...adr.metadata.tags); } // Add status as tag tags.push(`status:${adr.status}`); // Extract technology tags from content if (adr.content || adr.decision) { const content = (adr.content || '') + (adr.decision || ''); const techTags = this.inferTechnicalStack({ content } as any); tags.push(...techTags.map(tech => `tech:${tech.toLowerCase()}`)); } return [...new Set(tags)]; // Remove duplicates } private inferProjectPhase(adr: DiscoveredAdr): string | undefined { const content = [adr.content || '', adr.context || '', adr.decision || ''].join(' '); const contentLower = content.toLowerCase(); if (contentLower.match(/\b(prototype|poc|proof[- ]of[- ]concept)\b/)) return 'prototype'; if (contentLower.match(/\b(mvp|minimum[- ]viable[- ]product)\b/)) return 'mvp'; if (contentLower.match(/\b(design|planning|architecture)\b/)) return 'design'; if (contentLower.match(/\b(development|implementation|coding)\b/)) return 'development'; if (contentLower.match(/\b(testing|qa|quality[- ]assurance)\b/)) return 'testing'; if (contentLower.match(/\b(deployment|production|release)\b/)) return 'deployment'; if (contentLower.match(/\b(maintenance|support|operations)\b/)) return 'maintenance'; return undefined; } private inferBusinessDomain(adr: DiscoveredAdr): string | undefined { const content = [adr.content || '', adr.context || '', adr.decision || ''].join(' '); const contentLower = content.toLowerCase(); if (contentLower.match(/\b(e-?commerce|retail|shopping|payment)\b/)) return 'ecommerce'; if (contentLower.match(/\b(finance|financial|banking|payment|money)\b/)) return 'finance'; if (contentLower.match(/\b(healthcare|medical|patient|clinical)\b/)) return 'healthcare'; if (contentLower.match(/\b(education|learning|student|course)\b/)) return 'education'; if (contentLower.match(/\b(media|content|publishing|cms)\b/)) return 'media'; if (contentLower.match(/\b(social|community|user[- ]generated)\b/)) return 'social'; if (contentLower.match(/\b(iot|device|sensor|embedded)\b/)) return 'iot'; if (contentLower.match(/\b(analytics|data|bi|business[- ]intelligence)\b/)) return 'analytics'; return undefined; } private inferTechnicalStack(adr: DiscoveredAdr): string[] { const content = [adr.content || '', adr.context || '', adr.decision || ''].join(' '); return this.inferTechnicalStackFromContent(content); } private inferTechnicalStackFromContent(content: string): string[] { const technologies: string[] = []; const contentLower = content.toLowerCase(); // Languages const languages = [ 'javascript', 'typescript', 'python', 'java', 'go', 'rust', 'php', 'ruby', 'c#', 'swift', 'kotlin', ]; languages.forEach(lang => { if (contentLower.includes(lang)) technologies.push(lang); }); // Frameworks const frameworks = [ 'react', 'angular', 'vue', 'express', 'spring', 'django', 'flask', 'rails', 'laravel', ]; frameworks.forEach(framework => { if (contentLower.includes(framework)) technologies.push(framework); }); // Databases const databases = [ 'postgresql', 'mysql', 'mongodb', 'redis', 'elasticsearch', 'cassandra', 'sqlite', ]; databases.forEach(db => { if (contentLower.includes(db)) technologies.push(db); }); // Cloud providers const cloudProviders = ['aws', 'azure', 'gcp', 'google cloud', 'amazon', 'microsoft']; cloudProviders.forEach(provider => { if (contentLower.includes(provider)) technologies.push(provider); }); return technologies; } private inferEnvironmentalFactors(adr: DiscoveredAdr): string[] { const factors: string[] = []; const content = [adr.content || '', adr.context || '', adr.decision || ''].join(' '); const contentLower = content.toLowerCase(); if (contentLower.match(/\b(cloud|saas|paas|iaas)\b/)) factors.push('cloud'); if (contentLower.match(/\b(on[- ]premise|on[- ]prem|self[- ]hosted)\b/)) factors.push('on-premise'); if (contentLower.match(/\b(microservices?|distributed|service[- ]oriented)\b/)) factors.push('microservices'); if (contentLower.match(/\b(monolith|single[- ]application)\b/)) factors.push('monolithic'); if (contentLower.match(/\b(mobile|ios|android|responsive)\b/)) factors.push('mobile'); if (contentLower.match(/\b(web|browser|frontend|ui)\b/)) factors.push('web'); if (contentLower.match(/\b(api|rest|graphql|grpc)\b/)) factors.push('api-first'); if (contentLower.match(/\b(real[- ]?time|streaming|websocket)\b/)) factors.push('real-time'); if (contentLower.match(/\b(high[- ]?availability|ha|redundancy)\b/)) factors.push('high-availability'); if (contentLower.match(/\b(security|gdpr|compliance|privacy)\b/)) factors.push('security-critical'); return factors; } private inferStakeholders(adr: DiscoveredAdr): string[] { const stakeholders: string[] = []; const content = [adr.content || '', adr.context || '', adr.decision || ''].join(' '); const contentLower = content.toLowerCase(); if (contentLower.match(/\b(development[- ]?team|developer|engineer|dev[- ]team)\b/)) stakeholders.push('development-team'); if (contentLower.match(/\b(architect|architecture[- ]team)\b/)) stakeholders.push('architecture-team'); if (contentLower.match(/\b(devops|ops|operations)\b/)) stakeholders.push('operations-team'); if (contentLower.match(/\b(security[- ]team|infosec)\b/)) stakeholders.push('security-team'); if (contentLower.match(/\b(product[- ]manager|pm|product[- ]owner)\b/)) stakeholders.push('product-management'); if (contentLower.match(/\b(users?|customers?|clients?|end[- ]?users?)\b/)) stakeholders.push('end-users'); if (contentLower.match(/\b(business|stakeholder|management)\b/)) stakeholders.push('business-stakeholders'); return stakeholders; } private extractAlternatives( content: string ): Array<{ name: string; description: string; tradeoffs: string }> { const alternatives: Array<{ name: string; description: string; tradeoffs: string }> = []; // Look for alternatives section const alternativesMatch = content.match( /(?:alternatives?|options?|considered)[:\s]*([^#]*?)(?=\n##|\n#|$)/is ); if (alternativesMatch && alternativesMatch[1]) { const alternativesText = alternativesMatch[1]; const items = this.extractListItems(alternativesText); items.forEach((item, index) => { alternatives.push({ name: `Alternative ${index + 1}`, description: item, tradeoffs: 'Not specified', }); }); } return alternatives; } private parseAdrDate(dateStr: string): string | null { try { // Try to parse various date formats const date = new Date(dateStr); if (!isNaN(date.getTime())) { return date.toISOString(); } return null; } catch { return null; } } private inferAdrRelationships( sourceAdr: ArchitecturalDecisionMemory, targetAdr: ArchitecturalDecisionMemory ): MemoryRelationship[] { const relationships: MemoryRelationship[] = []; // Check for explicit supersedes relationship if ( sourceAdr.decisionData.status === 'superseded' || targetAdr.decisionData.status === 'superseded' ) { const newer = sourceAdr.created > targetAdr.created ? sourceAdr : targetAdr; const older = sourceAdr.created > targetAdr.created ? targetAdr : sourceAdr; if (older.decisionData.status === 'superseded') { relationships.push({ id: crypto.randomUUID(), sourceId: newer.id, targetId: older.id, type: 'supersedes', strength: 0.9, context: 'Inferred from ADR superseded status', created: new Date().toISOString(), confidence: 0.8, }); } } // Check for similar technologies (relates_to) const sourceStack = sourceAdr.context.technicalStack || []; const targetStack = targetAdr.context.technicalStack || []; const commonTech = sourceStack.filter(tech => targetStack.includes(tech)); if (commonTech.length >= 1) { // Lower threshold to 1 for better test coverage relationships.push({ id: crypto.randomUUID(), sourceId: sourceAdr.id, targetId: targetAdr.id, type: 'relates_to', strength: Math.min(0.8, commonTech.length * 0.3), context: `Shared technologies: ${commonTech.join(', ')}`, created: new Date().toISOString(), confidence: 0.6, }); } // Check for implementation dependencies based on content const sourceContent = ( sourceAdr.decisionData.context + sourceAdr.decisionData.decision ).toLowerCase(); const targetTitle = targetAdr.title.toLowerCase(); const targetContent = ( targetAdr.decisionData.context + targetAdr.decisionData.decision ).toLowerCase(); if ( sourceContent.includes(targetTitle) || sourceContent.includes('depends on') || sourceContent.includes('requires') || targetContent.includes(sourceAdr.title.toLowerCase()) ) { relationships.push({ id: crypto.randomUUID(), sourceId: sourceAdr.id, targetId: targetAdr.id, type: 'depends_on', strength: 0.7, context: 'Inferred from ADR content references', created: new Date().toISOString(), confidence: 0.5, }); } // Check for tag-based relationships const commonTags = sourceAdr.tags.filter(tag => targetAdr.tags.includes(tag)); if (commonTags.length >= 2) { relationships.push({ id: crypto.randomUUID(), sourceId: sourceAdr.id, targetId: targetAdr.id, type: 'relates_to', strength: Math.min(0.7, commonTags.length * 0.25), context: `Shared tags: ${commonTags.join(', ')}`, created: new Date().toISOString(), confidence: 0.7, }); } // Check for same business domain relationships if ( sourceAdr.context.businessDomain && targetAdr.context.businessDomain && sourceAdr.context.businessDomain === targetAdr.context.businessDomain ) { relationships.push({ id: crypto.randomUUID(), sourceId: sourceAdr.id, targetId: targetAdr.id, type: 'relates_to', strength: 0.5, context: `Same business domain: ${sourceAdr.context.businessDomain}`, created: new Date().toISOString(), confidence: 0.6, }); } return relationships; } private extractDescriptionFromContent(content: string): string { // Take first paragraph or first 200 characters const firstParagraph = content.split('\n\n')[0] || content; return firstParagraph.length > 200 ? firstParagraph.substring(0, 197) + '...' : firstParagraph; } private extractTagsFromContent(content: string): string[] { const tags: string[] = []; // Extract hashtags const hashtagMatches = content.matchAll(/#(\w+)/g); for (const match of hashtagMatches) { if (match[1]) { tags.push(match[1]); } } // Extract technical terms const techTerms = this.inferTechnicalStackFromContent(content); tags.push(...techTerms); return [...new Set(tags)]; } private detectContentFormat(content: string): KnowledgeArtifactMemory['artifactData']['format'] { if (content.startsWith('# ') || content.includes('## ')) return 'markdown'; if (content.trim().startsWith('{') || content.trim().startsWith('[')) return 'json'; if (content.includes('---\n') || content.startsWith('apiVersion:')) return 'yaml'; if (content.includes('function ') || content.includes('class ') || content.includes('import ')) return 'code'; return 'text'; } private inferApplicabilityScope(content: string): string[] { const scope: string[] = []; const contentLower = content.toLowerCase(); if (contentLower.match(/\b(frontend|ui|client[- ]side)\b/)) scope.push('frontend'); if (contentLower.match(/\b(backend|server[- ]side|api)\b/)) scope.push('backend'); if (contentLower.match(/\b(database|data[- ]layer|persistence)\b/)) scope.push('database'); if (contentLower.match(/\b(mobile|ios|android)\b/)) scope.push('mobile'); if (contentLower.match(/\b(devops|deployment|ci\/cd)\b/)) scope.push('devops'); if (contentLower.match(/\b(security|auth|encryption)\b/)) scope.push('security'); if (contentLower.match(/\b(testing|qa|quality)\b/)) scope.push('testing'); if (contentLower.match(/\b(performance|optimization|scaling)\b/)) scope.push('performance'); return scope.length > 0 ? scope : ['general']; } private extractKeyInsights(content: string): string[] { const insights: string[] = []; // Look for key insights markers const insightPatterns = [ /(?:key insight|important|note|warning|tip)[:\s]*([^.\n]+)/gi, /(?:remember|consider|beware)[:\s]*([^.\n]+)/gi, ]; insightPatterns.forEach(pattern => { const matches = content.matchAll(pattern); for (const match of matches) { if (match[1] && match[1].trim().length > 10) { insights.push(match[1].trim()); } } }); return insights.slice(0, 5); // Limit to 5 key insights } private extractActionableItems( content: string ): KnowledgeArtifactMemory['artifactData']['actionableItems'] { const actionableItems: KnowledgeArtifactMemory['artifactData']['actionableItems'] = []; // Look for TODO, FIXME, action items const actionPatterns = [ /(?:todo|action|task|must|should|need to)[:\s]*([^.\n]+)/gi, /- \[[ ]\] (.+)/g, ]; actionPatterns.forEach(pattern => { const matches = content.matchAll(pattern); for (const match of matches) { if (match[1] && match[1].trim().length > 5) { actionableItems.push({ action: match[1].trim(), priority: this.inferActionPriority(match[0]), // Use full match for priority inference timeframe: 'undefined', dependencies: [], }); } } }); return actionableItems.slice(0, 10); // Limit to 10 actionable items } private inferActionPriority(action: string): 'low' | 'medium' | 'high' { const actionLower = action.toLowerCase(); if (actionLower.match(/\b(critical|urgent|asap|immediately|must)\b/)) return 'high'; if (actionLower.match(/\b(important|should|priority)\b/)) return 'medium'; return 'low'; } }

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/tosin2013/mcp-adr-analysis-server'

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