@utaba/ucm-mcp-server
Version:
Universal Context Manager MCP Server - AI Productivity Platform
469 lines • 19.6 kB
JavaScript
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