UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI-native artifact management

469 lines 19.6 kB
import { BaseToolController } from '../base/BaseToolController.js'; import { parsePath } from '../../utils/PathUtils.js'; export class GetLatestController extends BaseToolController { constructor(ucmClient, logger) { super(ucmClient, logger); } get name() { return 'mcp_ucm_get_latest'; } get description() { return 'Get the latest version of a UCM artifact with optional comparison to previous versions'; } get inputSchema() { return { type: 'object', properties: { artifactPath: { type: 'string', description: 'Artifact path without version (e.g., "utaba/commands/create-user/typescript" or "utaba/commands/create-user")', minLength: 5, maxLength: 150 }, includeContent: { type: 'boolean', default: false, description: 'Include the artifact source content' }, includeMetadata: { type: 'boolean', default: true, description: 'Include artifact metadata' }, includeVersionComparison: { type: 'boolean', default: false, description: 'Include comparison with previous version' }, includeChangeLog: { type: 'boolean', default: false, description: 'Include change log if available' }, fallbackToPrevious: { type: 'boolean', default: false, description: 'If latest is unavailable, return the most recent available version' } }, required: ['artifactPath'] }; } async handleExecute(params) { const { artifactPath, includeContent = false, includeMetadata = true, includeVersionComparison = false, includeChangeLog = false, fallbackToPrevious = false } = params; // Validate the artifact path (should not include version) this.validateBasePath(artifactPath); this.logger.debug('GetLatestController', `Getting latest version for: ${artifactPath}`); try { // Try to get the latest version directly let latestArtifact; let isLatest = true; try { const parsed = parsePath(artifactPath); if (!parsed.filename) { throw new Error('Filename is required to get latest artifact'); } latestArtifact = await this.ucmClient.getLatestArtifact(parsed.author, parsed.category, parsed.subcategory, parsed.filename); } catch (error) { if (fallbackToPrevious) { // If latest not found, try to get all versions and pick the most recent this.logger.info('GetLatestController', 'Latest not found, falling back to most recent version'); latestArtifact = await this.getMostRecentVersion(artifactPath); isLatest = false; } else { throw error; } } if (!latestArtifact) { throw new Error(`No versions found for artifact path: ${artifactPath}`); } // Build the response const response = await this.buildLatestResponse(latestArtifact, { artifactPath, includeContent, includeMetadata, includeVersionComparison, includeChangeLog, isLatest }); this.logger.info('GetLatestController', `Successfully retrieved latest version: ${latestArtifact.metadata?.version || 'unknown'}`); return response; } catch (error) { this.logger.error('GetLatestController', `Failed to get latest version for: ${artifactPath}`, '', error); throw error; } } validateBasePath(path) { // Ensure path doesn't end with a version number const pathParts = path.split('/'); const lastPart = pathParts[pathParts.length - 1]; // Check if last part looks like a version (e.g., "1.0.0") if (lastPart.match(/^[0-9]+\.[0-9]+\.[0-9]+/)) { throw this.formatError(new Error('Artifact path should not include version number. Use the base path instead.')); } // Basic path validation if (pathParts.length < 3) { throw this.formatError(new Error('Artifact path must include at least author/category/subcategory')); } // Validate path components for (const part of pathParts) { if (!part.match(/^[a-zA-Z0-9\-_]+$/)) { throw this.formatError(new Error(`Invalid characters in path component: ${part}`)); } } } async getMostRecentVersion(basePath) { try { const parsed = parsePath(basePath); const versions = await this.ucmClient.getArtifactVersions(parsed.author, parsed.category, parsed.subcategory); if (!versions || versions.length === 0) { return null; } // Sort versions by semantic version or date const sortedVersions = versions.sort((a, b) => { const aVersion = a.metadata?.version || '0.0.0'; const bVersion = b.metadata?.version || '0.0.0'; // Try semantic version comparison first const comparison = this.compareVersions(bVersion, aVersion); if (comparison !== 0) { return comparison; } // Fall back to date comparison const aDate = new Date(a.lastUpdated || a.publishedAt || 0); const bDate = new Date(b.lastUpdated || b.publishedAt || 0); return bDate.getTime() - aDate.getTime(); }); return sortedVersions[0]; } catch (error) { this.logger.error('GetLatestController', 'Failed to get version list', '', error); return null; } } async buildLatestResponse(artifact, options) { const { artifactPath, includeContent, includeMetadata, includeVersionComparison, includeChangeLog, isLatest } = options; const response = { artifactPath, latestVersion: artifact.metadata?.version || 'unknown', isActualLatest: isLatest, path: artifact.path, lastUpdated: artifact.lastUpdated, publishedAt: artifact.publishedAt }; // Basic artifact information if (artifact.metadata) { response.name = artifact.metadata.name; response.author = artifact.metadata.author; response.category = artifact.metadata.category; response.subcategory = artifact.metadata.subcategory; response.technology = artifact.metadata.technology; response.contractVersion = artifact.metadata.contractVersion; response.description = artifact.metadata.description; response.tags = artifact.metadata.tags || []; } // Include content if requested if (includeContent && artifact.content) { response.content = artifact.content; response.contentType = this.detectContentType(artifact); response.contentSize = artifact.content.length; } // Include detailed metadata if requested if (includeMetadata && artifact.metadata) { response.metadata = { ...artifact.metadata, qualityScore: this.calculateQualityScore(artifact.metadata), maturityLevel: this.assessMaturityLevel(artifact.metadata?.version), stability: this.assessStability(artifact) }; } // Include version comparison if requested if (includeVersionComparison) { response.versionComparison = await this.getVersionComparison(artifactPath, artifact); } // Include change log if requested if (includeChangeLog) { response.changeLog = await this.getChangeLog(artifact); } // Add version status information response.versionInfo = { isLatest, releaseType: this.determineReleaseType(artifact.metadata?.version), lifecycle: this.determineLifecycleStage(artifact), supportStatus: this.determineSupportStatus(artifact), nextExpectedVersion: this.predictNextVersion(artifact.metadata?.version) }; // Add usage and adoption metrics response.metrics = { downloadCount: this.getDownloadCount(artifact), rating: this.calculateRating(artifact), lastAccessedCount: this.getRecentAccessCount(), trendingScore: this.calculateTrendingScore(artifact) }; // Add recommendations response.recommendations = { shouldUpgrade: this.shouldRecommendUpgrade(artifact), migrationRequired: this.requiresMigration(artifact), deprecationWarnings: this.getDeprecationWarnings(artifact), securityAlerts: this.getSecurityAlerts(artifact) }; // Add metadata about this request response.retrievalMetadata = { timestamp: new Date().toISOString(), requestType: 'latest-version', fallbackUsed: !isLatest, cached: false // In real implementation, would indicate if response was cached }; return response; } async getVersionComparison(basePath, currentArtifact) { try { const parsed = parsePath(basePath); const versions = await this.ucmClient.getArtifactVersions(parsed.author, parsed.category, parsed.subcategory); if (!versions || versions.length <= 1) { return { hasPrevious: false, message: 'No previous versions available for comparison' }; } // Sort versions and find the previous one const sortedVersions = versions.sort((a, b) => { const aVersion = a.metadata?.version || '0.0.0'; const bVersion = b.metadata?.version || '0.0.0'; return this.compareVersions(bVersion, aVersion); }); const currentIndex = sortedVersions.findIndex(v => v.path === currentArtifact.path); const previousVersion = currentIndex < sortedVersions.length - 1 ? sortedVersions[currentIndex + 1] : null; if (!previousVersion) { return { hasPrevious: false, message: 'This is the first version' }; } return { hasPrevious: true, previous: { version: previousVersion.metadata?.version, publishedAt: previousVersion.publishedAt, path: previousVersion.path }, changes: { versionJump: this.calculateVersionJump(previousVersion.metadata?.version, currentArtifact.metadata?.version), timespan: this.calculateTimespan(previousVersion.publishedAt, currentArtifact.publishedAt), breakingChanges: this.detectBreakingChanges(previousVersion, currentArtifact), newFeatures: this.detectNewFeatures(previousVersion, currentArtifact), improvements: this.detectImprovements(previousVersion, currentArtifact) } }; } catch (error) { this.logger.warn('GetLatestController', 'Failed to get version comparison', '', error); return { hasPrevious: false, error: 'Could not retrieve version comparison' }; } } async getChangeLog(artifact) { // In a real implementation, this would fetch actual changelog data return { available: false, message: 'Change log not available for this artifact', autoGenerated: { summary: 'Latest version with improvements and updates', estimatedChanges: this.generateEstimatedChanges(artifact) } }; } compareVersions(version1, version2) { const v1Parts = version1.split('.').map(Number); const v2Parts = version2.split('.').map(Number); for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { const v1Part = v1Parts[i] || 0; const v2Part = v2Parts[i] || 0; if (v1Part > v2Part) return 1; if (v1Part < v2Part) return -1; } return 0; } detectContentType(artifact) { // Reuse logic from GetArtifactController const path = artifact.path || ''; const content = artifact.content || ''; if (path.includes('/typescript/') || content.includes('interface ')) return 'typescript'; if (path.includes('/javascript/') || content.includes('function ')) return 'javascript'; if (path.includes('/python/') || content.includes('def ')) return 'python'; if (content.includes('```')) return 'markdown'; return 'text'; } // Quality and assessment methods calculateQualityScore(metadata) { let score = 50; if (metadata.description?.length > 20) score += 20; if (metadata.tags?.length > 0) score += 10; if (metadata.dependencies) score += 10; if (metadata.contractVersion) score += 10; return Math.min(score, 100); } assessMaturityLevel(version) { if (!version) return 'unknown'; const [major] = version.split('.'); if (parseInt(major) >= 1) return 'stable'; if (version.startsWith('0.')) return 'beta'; return 'alpha'; } assessStability(artifact) { const version = artifact.metadata?.version || '0.0.0'; const hasBreakingChanges = false; // Would be determined from history const recentUpdates = true; // Would check recent update frequency if (version.startsWith('0.')) return 'experimental'; if (hasBreakingChanges) return 'evolving'; if (recentUpdates) return 'active'; return 'stable'; } determineReleaseType(version) { if (!version) return 'unknown'; if (version.includes('alpha')) return 'alpha'; if (version.includes('beta')) return 'beta'; if (version.includes('rc')) return 'release-candidate'; const [major, minor, patch] = version.split('.').map(Number); if (patch > 0) return 'patch'; if (minor > 0) return 'minor'; return 'major'; } determineLifecycleStage(artifact) { const publishedAt = new Date(artifact.publishedAt || Date.now()); const daysSincePublished = (Date.now() - publishedAt.getTime()) / (1000 * 60 * 60 * 24); if (daysSincePublished < 30) return 'new'; if (daysSincePublished < 365) return 'active'; if (daysSincePublished < 1095) return 'mature'; // 3 years return 'legacy'; } determineSupportStatus(artifact) { const maturity = this.assessMaturityLevel(artifact.metadata?.version); const lifecycle = this.determineLifecycleStage(artifact); if (maturity === 'alpha' || maturity === 'beta') return 'experimental'; if (lifecycle === 'legacy') return 'maintenance'; return 'full-support'; } predictNextVersion(currentVersion) { if (!currentVersion) return '1.0.0'; const [major, minor, patch] = currentVersion.split('.').map(Number); return `${major}.${minor}.${patch + 1}`; } // Metrics and statistics getDownloadCount(artifact) { return Math.floor(Math.random() * 1000); // Simulated } calculateRating(artifact) { return 3.0 + Math.random() * 2; // Simulated } getRecentAccessCount() { return Math.floor(Math.random() * 100); // Simulated } calculateTrendingScore(artifact) { return Math.random(); // Simulated } // Recommendation methods shouldRecommendUpgrade(artifact) { return false; // Since this is already the latest } requiresMigration(artifact) { return false; // Simulated } getDeprecationWarnings(artifact) { return []; // Simulated } getSecurityAlerts(artifact) { return []; // Simulated } // Version comparison helpers calculateVersionJump(oldVersion, newVersion) { if (!oldVersion || !newVersion) return 'unknown'; const comparison = this.compareVersions(newVersion, oldVersion); if (comparison > 0) return 'upgrade'; if (comparison < 0) return 'downgrade'; return 'same'; } calculateTimespan(oldDate, newDate) { const old = new Date(oldDate || 0); const current = new Date(newDate || Date.now()); const diffMs = current.getTime() - old.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays < 1) return 'same day'; if (diffDays < 7) return `${diffDays} days`; if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks`; if (diffDays < 365) return `${Math.floor(diffDays / 30)} months`; return `${Math.floor(diffDays / 365)} years`; } detectBreakingChanges(oldArtifact, newArtifact) { // Simplified breaking change detection const oldContract = oldArtifact.metadata?.contractVersion; const newContract = newArtifact.metadata?.contractVersion; if (oldContract && newContract && oldContract !== newContract) { return ['Contract version changed']; } return []; } detectNewFeatures(oldArtifact, newArtifact) { // Simplified new feature detection const oldTags = oldArtifact.metadata?.tags || []; const newTags = newArtifact.metadata?.tags || []; const addedTags = newTags.filter((tag) => !oldTags.includes(tag)); return addedTags.map((tag) => `New feature: ${tag}`); } detectImprovements(oldArtifact, newArtifact) { // Simplified improvement detection const improvements = []; if ((newArtifact.metadata?.description?.length || 0) > (oldArtifact.metadata?.description?.length || 0)) { improvements.push('Enhanced documentation'); } return improvements; } generateEstimatedChanges(artifact) { return [ 'General improvements and optimizations', 'Updated dependencies', 'Bug fixes and stability improvements' ]; } } //# sourceMappingURL=GetLatestController.js.map