UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI-native artifact management

131 lines 5.66 kB
import { BaseToolController } from '../base/BaseToolController.js'; import { ValidationUtils } from '../../utils/ValidationUtils.js'; export class FindByPurposeController extends BaseToolController { constructor(ucmClient, logger) { super(ucmClient, logger); } get name() { return 'mcp_ucm_find_by_purpose'; } get description() { return 'Find UCM artifacts by their intended purpose or use case using semantic search'; } get inputSchema() { return { type: 'object', properties: { purpose: { type: 'string', description: 'Describe what you want to accomplish (e.g., "create user account", "send email notification")', minLength: 3, maxLength: 500 }, category: { type: 'string', enum: ['commands', 'services', 'patterns', 'implementations', 'contracts', 'guidance'], description: 'Filter results by artifact category' }, technology: { type: 'string', description: 'Filter by technology stack (e.g., "typescript", "python", "nextjs")', pattern: '^[a-zA-Z0-9\\-_]+$' }, maxResults: { type: 'number', default: 10, minimum: 1, maximum: 50, description: 'Maximum number of results to return' } }, required: ['purpose'] }; } async handleExecute(params) { const { purpose, category, technology, maxResults = 10 } = params; // Sanitize the search query const sanitizedPurpose = ValidationUtils.sanitizeSearchQuery(purpose); // Validate optional category if (category) { ValidationUtils.validateCategory(category); } this.logger.debug('FindByPurposeController', `Searching for purpose: "${sanitizedPurpose}"`); try { // Build search filters const filters = {}; if (category) filters.category = category; if (technology) filters.technology = technology; if (maxResults) filters.limit = maxResults; // Use semantic search through UCM API const results = await this.ucmClient.searchArtifacts({ ...filters, // Note: Our current API doesn't support text search, using category filter instead category: filters.category }); // Transform results to include relevance scoring const transformedResults = results.slice(0, maxResults).map((artifact, index) => ({ name: artifact.metadata?.name || 'Unknown', path: artifact.path, description: artifact.metadata?.description || '', category: artifact.metadata?.category || '', subcategory: artifact.metadata?.subcategory || '', technology: artifact.metadata?.technology || 'technology-agnostic', version: artifact.metadata?.version || '', author: artifact.metadata?.author || '', tags: artifact.metadata?.tags || [], relevanceScore: this.calculateRelevanceScore(sanitizedPurpose, artifact, index), lastUpdated: artifact.lastUpdated })); // Sort by relevance score (highest first) transformedResults.sort((a, b) => b.relevanceScore - a.relevanceScore); this.logger.info('FindByPurposeController', `Found ${transformedResults.length} artifacts for purpose search`); return { results: transformedResults, query: { purpose: sanitizedPurpose, category, technology, maxResults }, totalFound: results.length, searchMetadata: { searchType: 'purpose-based', timestamp: new Date().toISOString(), processingTimeMs: Date.now() - Date.now() // Simplified for demo } }; } catch (error) { this.logger.error('FindByPurposeController', 'Purpose search failed', '', error); throw error; } } calculateRelevanceScore(purpose, artifact, position) { let score = 1.0 - (position * 0.1); // Base score decreases by position const purposeLower = purpose.toLowerCase(); const description = (artifact.metadata?.description || '').toLowerCase(); const name = (artifact.metadata?.name || '').toLowerCase(); const tags = artifact.metadata?.tags || []; // Boost score for exact matches in name if (name.includes(purposeLower)) { score += 0.5; } // Boost score for matches in description if (description.includes(purposeLower)) { score += 0.3; } // Boost score for tag matches const purposeWords = purposeLower.split(' '); for (const word of purposeWords) { if (tags.some((tag) => tag.toLowerCase().includes(word))) { score += 0.2; } } // Ensure score stays within reasonable bounds return Math.min(Math.max(score, 0.1), 1.0); } } //# sourceMappingURL=FindByPurposeController.js.map