UNPKG

ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI-native artifact management

465 lines 19.9 kB
import { BaseToolController } from '../base/BaseToolController.js'; import { ValidationUtils } from '../../utils/ValidationUtils.js'; import { parsePath } from '../../utils/PathUtils.js'; export class ValidatePathController extends BaseToolController { constructor(ucmClient, logger) { super(ucmClient, logger); } get name() { return 'mcp_ucm_validate_path'; } get description() { return 'Validate UCM artifact path format and optionally check for existence in the repository'; } get inputSchema() { return { type: 'object', properties: { path: { type: 'string', description: 'Artifact path to validate (e.g., "utaba/commands/create-user/typescript/1.0.0")', minLength: 1, maxLength: 200 }, checkExistence: { type: 'boolean', default: false, description: 'Check if the artifact actually exists in the repository' }, validateComponents: { type: 'boolean', default: true, description: 'Validate individual path components (author, category, etc.)' }, suggestCorrections: { type: 'boolean', default: true, description: 'Provide suggestions for fixing invalid paths' }, checkSimilar: { type: 'boolean', default: false, description: 'Find similar existing paths if validation fails' }, strictMode: { type: 'boolean', default: false, description: 'Enable strict validation rules (more restrictive)' } }, required: ['path'] }; } async handleExecute(params) { const { path, checkExistence = false, validateComponents = true, suggestCorrections = true, checkSimilar = false, strictMode = false } = params; this.logger.debug('ValidatePathController', `Validating path: ${path}`); try { // Perform comprehensive validation const validationResult = await this.performPathValidation(path, { checkExistence, validateComponents, suggestCorrections, checkSimilar, strictMode }); this.logger.info('ValidatePathController', `Path validation completed: ${validationResult.isValid ? 'VALID' : 'INVALID'}`); return validationResult; } catch (error) { this.logger.error('ValidatePathController', `Path validation failed for: ${path}`, '', error); throw error; } } async performPathValidation(path, options) { const result = { path, isValid: true, errors: [], warnings: [], suggestions: [], pathAnalysis: {}, existence: null, similar: [], metadata: { timestamp: new Date().toISOString(), validationType: options.strictMode ? 'strict' : 'standard', checksPerformed: [] } }; // 1. Basic format validation await this.validateBasicFormat(path, result, options); // 2. Component validation if (options.validateComponents && result.pathAnalysis.components) { await this.validateComponents(result.pathAnalysis.components, result, options); } // 3. Semantic validation await this.validateSemantics(path, result, options); // 4. Existence check if (options.checkExistence) { await this.checkPathExistence(path, result); } // 5. Find similar paths if invalid and requested if (!result.isValid && options.checkSimilar) { await this.findSimilarPaths(path, result); } // 6. Generate suggestions if requested if (options.suggestCorrections && (!result.isValid || result.warnings.length > 0)) { await this.generateSuggestions(path, result, options); } // Set overall validity result.isValid = result.errors.length === 0; return result; } async validateBasicFormat(path, result, options) { result.metadata.checksPerformed.push('basic-format'); // Basic checks if (!path || typeof path !== 'string') { result.errors.push('Path must be a non-empty string'); return; } if (path.trim() !== path) { result.errors.push('Path cannot have leading or trailing whitespace'); } if (path.includes('//')) { result.errors.push('Path cannot contain double slashes'); } if (path.includes('..')) { result.errors.push('Path cannot contain relative path components (..)'); } if (path.startsWith('/') || path.endsWith('/')) { result.errors.push('Path cannot start or end with forward slash'); } // Character validation const validCharPattern = /^[a-zA-Z0-9\-_.\/]+$/; if (!validCharPattern.test(path)) { result.errors.push('Path contains invalid characters. Only alphanumeric, hyphens, underscores, dots, and forward slashes are allowed'); } // Parse path components const components = path.split('/'); result.pathAnalysis = { components: this.parsePathComponents(components), componentCount: components.length, pathType: this.determinePathType(components), isVersioned: this.isVersionedPath(components) }; // Length validation if (path.length > 200) { result.errors.push('Path exceeds maximum length of 200 characters'); } if (components.length < 3) { result.errors.push('Path must have at least 3 components (author/category/subcategory)'); } if (components.length > 5) { result.errors.push('Path has too many components. Maximum is 5 (author/category/subcategory/technology/version)'); } // Strict mode additional checks if (options.strictMode) { this.applyStrictModeValidation(path, components, result); } } parsePathComponents(components) { const parsed = { author: components[0] || null, category: components[1] || null, subcategory: components[2] || null, technology: null, version: null }; if (components.length === 4) { // Could be either technology-agnostic (author/category/subcategory/version) // or missing version (author/category/subcategory/technology) if (this.looksLikeVersion(components[3])) { parsed.version = components[3]; } else { parsed.technology = components[3]; } } else if (components.length === 5) { // author/category/subcategory/technology/version parsed.technology = components[3]; parsed.version = components[4]; } return parsed; } determinePathType(components) { if (components.length === 4 && this.looksLikeVersion(components[3])) { return 'technology-agnostic-versioned'; } else if (components.length === 4) { return 'technology-specific-unversioned'; } else if (components.length === 5) { return 'technology-specific-versioned'; } else if (components.length === 3) { return 'subcategory-level'; } else if (components.length === 2) { return 'category-level'; } else if (components.length === 1) { return 'author-level'; } return 'unknown'; } isVersionedPath(components) { const lastComponent = components[components.length - 1]; return this.looksLikeVersion(lastComponent); } looksLikeVersion(component) { return /^[0-9]+\.[0-9]+\.[0-9]+/.test(component); } async validateComponents(components, result, options) { result.metadata.checksPerformed.push('component-validation'); // Validate author if (components.author) { if (!this.isValidAuthorId(components.author)) { result.errors.push(`Invalid author ID format: ${components.author}`); } } // Validate category if (components.category) { try { ValidationUtils.validateCategory(components.category); } catch (error) { result.errors.push(`Invalid category: ${components.category}`); } } // Validate subcategory if (components.subcategory) { if (!this.isValidSubcategory(components.subcategory)) { result.errors.push(`Invalid subcategory format: ${components.subcategory}`); } } // Validate technology if (components.technology) { if (!this.isValidTechnology(components.technology)) { result.errors.push(`Invalid technology identifier: ${components.technology}`); } } // Validate version if (components.version) { try { ValidationUtils.validateVersion(components.version); } catch (error) { result.errors.push(`Invalid version format: ${components.version}`); } } // Component consistency checks this.validateComponentConsistency(components, result); } async validateSemantics(path, result, options) { result.metadata.checksPerformed.push('semantic-validation'); const components = result.pathAnalysis.components; // Check for common naming patterns and best practices if (components.subcategory) { if (components.subcategory.includes('_') && components.subcategory.includes('-')) { result.warnings.push('Subcategory uses both underscores and hyphens - consider consistent naming'); } if (components.subcategory.length > 50) { result.warnings.push('Subcategory name is very long - consider shortening for better usability'); } } // Technology-specific validations if (components.technology) { const knownTechnologies = [ 'typescript', 'javascript', 'python', 'java', 'csharp', 'go', 'rust', 'nextjs', 'react', 'vue', 'angular', 'nodejs', 'deno' ]; if (!knownTechnologies.includes(components.technology.toLowerCase())) { result.warnings.push(`Technology '${components.technology}' is not in the list of commonly used technologies`); } } // Version semantic checks if (components.version) { if (components.version.startsWith('0.')) { result.warnings.push('Version indicates pre-release (0.x.x) - ensure this is intentional'); } if (components.version.includes('alpha') || components.version.includes('beta')) { result.warnings.push('Version indicates pre-release software'); } } // Path semantics if (result.pathAnalysis.pathType === 'technology-specific-unversioned') { result.warnings.push('Path appears to specify technology but no version - consider adding version'); } } async checkPathExistence(path, result) { result.metadata.checksPerformed.push('existence-check'); try { const parsed = parsePath(path); if (!parsed.filename) { throw new Error('Filename is required to check artifact existence'); } const artifact = await this.ucmClient.getArtifact(parsed.author, parsed.category, parsed.subcategory, parsed.filename, parsed.version); result.existence = { exists: true, artifact: { id: artifact.id, name: artifact.metadata?.name, lastUpdated: artifact.lastUpdated, publishedAt: artifact.publishedAt } }; } catch (error) { result.existence = { exists: false, error: error instanceof Error ? error.message : String(error) }; if (result.pathAnalysis.isVersioned) { // Try to check if a non-versioned path exists await this.checkParentPathExistence(path, result); } } } async checkParentPathExistence(path, result) { try { const pathParts = path.split('/'); const parentPath = pathParts.slice(0, -1).join('/'); if (parentPath) { const parsed = parsePath(parentPath); const versions = await this.ucmClient.getArtifactVersions(parsed.author, parsed.category, parsed.subcategory); if (versions && versions.length > 0) { result.existence.parentExists = true; result.existence.availableVersions = versions.map(v => v.metadata?.version).filter(Boolean); result.suggestions.push(`Artifact exists but not this version. Available versions: ${result.existence.availableVersions.join(', ')}`); } } } catch (error) { // Parent doesn't exist either result.existence.parentExists = false; } } async findSimilarPaths(path, result) { result.metadata.checksPerformed.push('similar-paths'); try { // Extract search terms from the path const components = result.pathAnalysis.components; const searchTerms = [ components.author, components.category, components.subcategory ].filter(Boolean).join(' '); if (searchTerms) { const searchResults = await this.ucmClient.searchArtifacts({ category: components.category, subcategory: components.subcategory, limit: 5 }); result.similar = searchResults.map(artifact => ({ path: artifact.path, name: artifact.metadata?.name, similarity: this.calculatePathSimilarity(path, artifact.path), reason: this.generateSimilarityReason(path, artifact.path) })).filter(item => item.similarity > 0.3) .sort((a, b) => b.similarity - a.similarity); } } catch (error) { result.similar = []; this.logger.debug('ValidatePathController', 'Could not find similar paths', '', error instanceof Error ? error.message : String(error)); } } async generateSuggestions(path, result, options) { const suggestions = result.suggestions; // Format-based suggestions if (result.errors.some((e) => e.includes('invalid characters'))) { suggestions.push('Remove special characters and use only alphanumeric, hyphens, underscores, and dots'); } if (result.errors.some((e) => e.includes('double slashes'))) { suggestions.push('Remove double slashes from the path'); } // Component-based suggestions const components = result.pathAnalysis.components; if (components.author && !this.isValidAuthorId(components.author)) { suggestions.push('Author ID should be lowercase alphanumeric with hyphens/underscores only'); } if (result.pathAnalysis.pathType === 'technology-specific-unversioned') { suggestions.push('Consider adding a version number as the last component'); } // Existence-based suggestions if (result.existence && !result.existence.exists && result.existence.availableVersions) { const versions = result.existence.availableVersions; if (versions.length > 0) { suggestions.push(`Try one of these existing versions: ${versions.slice(0, 3).join(', ')}`); } } // Similar path suggestions if (result.similar && result.similar.length > 0) { const topSimilar = result.similar[0]; suggestions.push(`Did you mean: ${topSimilar.path}? (${topSimilar.reason})`); } } applyStrictModeValidation(path, components, result) { // Strict mode rules for (const component of components) { if (component.length < 2) { result.errors.push('In strict mode, all path components must be at least 2 characters long'); } if (component.includes('_') && component.includes('-')) { result.errors.push('In strict mode, components cannot mix underscores and hyphens'); } if (!/^[a-z0-9\-_\.]+$/.test(component)) { result.errors.push('In strict mode, components must be lowercase'); } } // Must be versioned in strict mode if (!result.pathAnalysis.isVersioned) { result.errors.push('In strict mode, paths must include a version number'); } } validateComponentConsistency(components, result) { // Check for naming consistency if (components.subcategory && components.technology) { if (components.subcategory.toLowerCase().includes(components.technology.toLowerCase())) { result.warnings.push('Subcategory name includes technology - this may be redundant'); } } // Check for reasonable relationships if (components.category === 'commands' && components.technology === 'html') { result.warnings.push('HTML technology unusual for commands category'); } } // Helper validation methods isValidAuthorId(authorId) { return /^[a-zA-Z0-9\-_]+$/.test(authorId) && authorId.length >= 2 && authorId.length <= 50; } isValidSubcategory(subcategory) { return /^[a-zA-Z0-9\-_]+$/.test(subcategory) && subcategory.length >= 2 && subcategory.length <= 50; } isValidTechnology(technology) { return /^[a-zA-Z0-9\-_]+$/.test(technology) && technology.length >= 2 && technology.length <= 20; } calculatePathSimilarity(path1, path2) { const components1 = path1.split('/'); const components2 = path2.split('/'); let matches = 0; const maxLength = Math.max(components1.length, components2.length); for (let i = 0; i < Math.min(components1.length, components2.length); i++) { if (components1[i].toLowerCase() === components2[i].toLowerCase()) { matches++; } } return matches / maxLength; } generateSimilarityReason(originalPath, similarPath) { const orig = originalPath.split('/'); const sim = similarPath.split('/'); if (orig[0] === sim[0]) { if (orig[1] === sim[1]) { return 'Same author and category'; } return 'Same author'; } if (orig[1] === sim[1]) { return 'Same category'; } return 'Similar structure'; } } //# sourceMappingURL=ValidatePathController.js.map