@utaba/ucm-mcp-server
Version:
Universal Context Manager MCP Server - AI-native artifact management
530 lines • 22.1 kB
JavaScript
import { BaseToolController } from '../base/BaseToolController.js';
import { ValidationUtils } from '../../utils/ValidationUtils.js';
import { parsePath } from '../../utils/PathUtils.js';
export class ListVersionsController extends BaseToolController {
constructor(ucmClient, logger) {
super(ucmClient, logger);
}
get name() {
return 'mcp_ucm_list_versions';
}
get description() {
return 'List all available versions of a UCM artifact with detailed version information and comparison';
}
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
},
includeMetadata: {
type: 'boolean',
default: false,
description: 'Include detailed metadata for each version'
},
includeChangeSummary: {
type: 'boolean',
default: true,
description: 'Include summary of changes between versions'
},
includeStats: {
type: 'boolean',
default: false,
description: 'Include download and usage statistics for each version'
},
sortBy: {
type: 'string',
enum: ['version', 'date', 'downloads', 'rating'],
default: 'version',
description: 'Sort versions by specified criteria'
},
sortOrder: {
type: 'string',
enum: ['asc', 'desc'],
default: 'desc',
description: 'Sort order (newest first by default)'
},
limit: {
type: 'number',
minimum: 1,
maximum: 100,
default: 20,
description: 'Maximum number of versions to return'
},
sinceVersion: {
type: 'string',
pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+',
description: 'Only show versions newer than this version'
},
versionPattern: {
type: 'string',
description: 'Filter versions matching this pattern (e.g., "1.*", "*.0.0")'
}
},
required: ['artifactPath']
};
}
async handleExecute(params) {
const { artifactPath, includeMetadata = false, includeChangeSummary = true, includeStats = false, sortBy = 'version', sortOrder = 'desc', limit = 20, sinceVersion, versionPattern } = params;
// Validate the artifact path (should not include version)
this.validateBasePath(artifactPath);
if (sinceVersion) {
ValidationUtils.validateVersion(sinceVersion);
}
this.logger.debug('ListVersionsController', `Listing versions for: ${artifactPath}`);
try {
// Parse the path to get component parts
const parsed = parsePath(artifactPath);
// Get all versions from UCM API
const allVersions = await this.ucmClient.getArtifactVersions(parsed.author, parsed.category, parsed.subcategory);
if (!allVersions || allVersions.length === 0) {
return {
artifactPath,
versions: [],
totalVersions: 0,
message: 'No versions found for this artifact'
};
}
// Apply filters
let filteredVersions = allVersions;
if (sinceVersion) {
filteredVersions = this.filterVersionsSince(filteredVersions, sinceVersion);
}
if (versionPattern) {
filteredVersions = this.filterVersionsByPattern(filteredVersions, versionPattern);
}
// Process and enrich version data
const enrichedVersions = await this.enrichVersionData(filteredVersions, { includeMetadata, includeChangeSummary, includeStats });
// Sort versions
const sortedVersions = this.sortVersions(enrichedVersions, sortBy, sortOrder);
// Apply limit
const limitedVersions = sortedVersions.slice(0, limit);
// Build response
const response = {
artifactPath,
versions: limitedVersions,
pagination: {
total: filteredVersions.length,
returned: limitedVersions.length,
hasMore: filteredVersions.length > limit
},
sorting: {
sortBy,
sortOrder
},
filters: {
sinceVersion,
versionPattern
},
summary: this.generateVersionSummary(allVersions, filteredVersions),
versionAnalysis: this.analyzeVersionHistory(sortedVersions),
recommendations: this.generateVersionRecommendations(sortedVersions),
metadata: {
timestamp: new Date().toISOString(),
includeMetadata,
includeChangeSummary,
includeStats
}
};
this.logger.info('ListVersionsController', `Listed ${limitedVersions.length} versions (${filteredVersions.length} total)`);
return response;
}
catch (error) {
this.logger.error('ListVersionsController', `Failed to list versions 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];
if (lastPart.match(/^[0-9]+\.[0-9]+\.[0-9]+/)) {
throw this.formatError(new Error('Artifact path should not include version number'));
}
if (pathParts.length < 3) {
throw this.formatError(new Error('Artifact path must include at least author/category/subcategory'));
}
}
filterVersionsSince(versions, sinceVersion) {
return versions.filter(version => {
const versionNumber = version.metadata?.version || '0.0.0';
return this.compareVersions(versionNumber, sinceVersion) > 0;
});
}
filterVersionsByPattern(versions, pattern) {
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\./g, '\\.'));
return versions.filter(version => {
const versionNumber = version.metadata?.version || '0.0.0';
return regex.test(versionNumber);
});
}
async enrichVersionData(versions, options) {
const { includeMetadata, includeChangeSummary, includeStats } = options;
const enrichedVersions = [];
for (let i = 0; i < versions.length; i++) {
const version = versions[i];
const enriched = {
version: version.metadata?.version || 'unknown',
path: version.path,
publishedAt: version.publishedAt,
lastUpdated: version.lastUpdated,
contractVersion: version.metadata?.contractVersion,
// Basic version information
releaseType: this.determineReleaseType(version.metadata?.version),
isPreRelease: this.isPreRelease(version.metadata?.version),
lifecycle: this.determineLifecycleStage(version),
stability: this.assessVersionStability(version)
};
// Include metadata if requested
if (includeMetadata && version.metadata) {
enriched.metadata = {
...version.metadata,
qualityScore: this.calculateQualityScore(version.metadata),
completeness: this.assessMetadataCompleteness(version.metadata)
};
}
// Include change summary if requested
if (includeChangeSummary) {
const previousVersion = i < versions.length - 1 ? versions[i + 1] : null;
enriched.changeSummary = await this.generateChangeSummary(version, previousVersion);
}
// Include statistics if requested
if (includeStats) {
enriched.statistics = {
downloadCount: this.getDownloadCount(version),
rating: this.calculateRating(version),
usageMetrics: this.getUsageMetrics(version),
adoptionRate: this.calculateAdoptionRate(version, versions)
};
}
enrichedVersions.push(enriched);
}
return enrichedVersions;
}
sortVersions(versions, sortBy, sortOrder) {
return versions.sort((a, b) => {
let comparison = 0;
switch (sortBy) {
case 'version':
comparison = this.compareVersions(a.version, b.version);
break;
case 'date':
const aDate = new Date(a.publishedAt || 0);
const bDate = new Date(b.publishedAt || 0);
comparison = aDate.getTime() - bDate.getTime();
break;
case 'downloads':
const aDownloads = a.statistics?.downloadCount || 0;
const bDownloads = b.statistics?.downloadCount || 0;
comparison = aDownloads - bDownloads;
break;
case 'rating':
const aRating = a.statistics?.rating || 0;
const bRating = b.statistics?.rating || 0;
comparison = aRating - bRating;
break;
default:
comparison = this.compareVersions(a.version, b.version);
}
return sortOrder === 'desc' ? -comparison : comparison;
});
}
generateVersionSummary(allVersions, filteredVersions) {
const versionNumbers = allVersions.map(v => v.metadata?.version || '0.0.0');
const latest = this.getLatestVersion(versionNumbers);
const oldest = this.getOldestVersion(versionNumbers);
return {
totalVersions: allVersions.length,
filteredVersions: filteredVersions.length,
latestVersion: latest,
oldestVersion: oldest,
versionSpan: this.calculateVersionSpan(oldest, latest),
releaseFrequency: this.calculateReleaseFrequency(allVersions),
averageTimeBetweenReleases: this.calculateAverageTimeBetweenReleases(allVersions)
};
}
analyzeVersionHistory(versions) {
const analysis = {
patterns: {
majorReleases: 0,
minorReleases: 0,
patchReleases: 0,
preReleases: 0
},
trends: {
releaseVelocity: 'stable',
breakingChangeFrequency: 'low',
stabilityTrend: 'improving'
},
insights: []
};
// Analyze release patterns
versions.forEach(version => {
switch (version.releaseType) {
case 'major':
analysis.patterns.majorReleases++;
break;
case 'minor':
analysis.patterns.minorReleases++;
break;
case 'patch':
analysis.patterns.patchReleases++;
break;
default:
if (version.isPreRelease) {
analysis.patterns.preReleases++;
}
}
});
// Generate insights
if (analysis.patterns.majorReleases > versions.length * 0.3) {
analysis.insights.push('Frequent major releases - API may be evolving rapidly');
}
if (analysis.patterns.patchReleases > versions.length * 0.6) {
analysis.insights.push('High patch release frequency - good maintenance practices');
}
if (analysis.patterns.preReleases > 0) {
analysis.insights.push('Pre-release versions available - bleeding edge features');
}
return analysis;
}
generateVersionRecommendations(versions) {
const recommendations = {
recommended: null,
alternatives: [],
warnings: []
};
if (versions.length === 0) {
return recommendations;
}
// Find recommended version (latest stable)
const stableVersions = versions.filter(v => !v.isPreRelease && v.stability !== 'experimental');
if (stableVersions.length > 0) {
recommendations.recommended = {
version: stableVersions[0].version,
reason: 'Latest stable version',
confidence: 'high'
};
}
else {
recommendations.recommended = {
version: versions[0].version,
reason: 'Latest available version',
confidence: 'medium'
};
}
// Find alternatives
const majorVersions = this.groupByMajorVersion(versions);
for (const [major, versionList] of Object.entries(majorVersions)) {
if (versionList.length > 0 && versionList[0].version !== recommendations.recommended.version) {
recommendations.alternatives.push({
version: versionList[0].version,
reason: `Latest in v${major}.x series`,
stability: versionList[0].stability
});
}
}
// Generate warnings
const latestVersion = versions[0];
if (latestVersion?.isPreRelease) {
recommendations.warnings.push('Latest version is a pre-release - use with caution in production');
}
if (latestVersion?.lifecycle === 'legacy') {
recommendations.warnings.push('This artifact may be deprecated - consider alternatives');
}
return recommendations;
}
async generateChangeSummary(version, previousVersion) {
if (!previousVersion) {
return {
isFirst: true,
message: 'Initial version'
};
}
const summary = {
isFirst: false,
previousVersion: previousVersion.metadata?.version,
changeType: this.determineChangeType(previousVersion.metadata?.version, version.metadata?.version),
timespan: this.calculateTimespan(previousVersion.publishedAt, version.publishedAt),
breaking: this.hasBreakingChanges(previousVersion, version),
features: this.detectNewFeatures(previousVersion, version),
improvements: this.detectImprovements(previousVersion, version),
fixes: this.detectFixes(previousVersion, version)
};
return summary;
}
// Helper methods
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 v1Part - v2Part;
}
}
return 0;
}
determineReleaseType(version) {
if (!version)
return 'unknown';
const [major, minor, patch] = version.split('.').map(Number);
if (major > 0 && minor === 0 && patch === 0)
return 'major';
if (minor > 0 && patch === 0)
return 'minor';
return 'patch';
}
isPreRelease(version) {
return !!(version && (version.includes('alpha') || version.includes('beta') || version.includes('rc')));
}
determineLifecycleStage(version) {
const publishedAt = new Date(version.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';
return 'legacy';
}
assessVersionStability(version) {
if (this.isPreRelease(version.metadata?.version))
return 'experimental';
if (this.determineLifecycleStage(version) === 'new')
return 'testing';
return 'stable';
}
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);
}
assessMetadataCompleteness(metadata) {
const fields = ['name', 'description', 'author', 'category', 'version'];
const presentFields = fields.filter(field => metadata[field]);
return Math.round((presentFields.length / fields.length) * 100);
}
getLatestVersion(versions) {
return versions.sort((a, b) => this.compareVersions(b, a))[0];
}
getOldestVersion(versions) {
return versions.sort((a, b) => this.compareVersions(a, b))[0];
}
calculateVersionSpan(oldest, latest) {
const comparison = this.compareVersions(latest, oldest);
if (comparison === 0)
return 'Single version';
const [oldMajor] = oldest.split('.').map(Number);
const [newMajor] = latest.split('.').map(Number);
const majorSpan = newMajor - oldMajor;
if (majorSpan > 0)
return `${majorSpan} major version${majorSpan > 1 ? 's' : ''}`;
return 'Minor/patch versions';
}
calculateReleaseFrequency(versions) {
if (versions.length < 2)
return 'insufficient-data';
const timespan = this.calculateAverageTimeBetweenReleases(versions);
if (timespan < 30)
return 'high';
if (timespan < 90)
return 'moderate';
return 'low';
}
calculateAverageTimeBetweenReleases(versions) {
if (versions.length < 2)
return 0;
const dates = versions
.map(v => new Date(v.publishedAt || 0))
.sort((a, b) => a.getTime() - b.getTime());
let totalDays = 0;
for (let i = 1; i < dates.length; i++) {
totalDays += (dates[i].getTime() - dates[i - 1].getTime()) / (1000 * 60 * 60 * 24);
}
return totalDays / (dates.length - 1);
}
groupByMajorVersion(versions) {
const groups = {};
versions.forEach(version => {
const [major] = version.version.split('.');
if (!groups[major])
groups[major] = [];
groups[major].push(version);
});
return groups;
}
determineChangeType(oldVersion, newVersion) {
const comparison = this.compareVersions(newVersion, oldVersion);
const [oldMajor, oldMinor] = oldVersion.split('.').map(Number);
const [newMajor, newMinor] = newVersion.split('.').map(Number);
if (newMajor > oldMajor)
return 'major';
if (newMinor > oldMinor)
return 'minor';
return 'patch';
}
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`;
}
// Simulated methods for demo
hasBreakingChanges(oldVersion, newVersion) {
return oldVersion.metadata?.contractVersion !== newVersion.metadata?.contractVersion;
}
detectNewFeatures(oldVersion, newVersion) {
const oldTags = oldVersion.metadata?.tags || [];
const newTags = newVersion.metadata?.tags || [];
return newTags.filter((tag) => !oldTags.includes(tag));
}
detectImprovements(oldVersion, newVersion) {
const improvements = [];
if ((newVersion.metadata?.description?.length || 0) > (oldVersion.metadata?.description?.length || 0)) {
improvements.push('Enhanced documentation');
}
return improvements;
}
detectFixes(oldVersion, newVersion) {
return ['General bug fixes and improvements']; // Simulated
}
getDownloadCount(version) {
return Math.floor(Math.random() * 1000); // Simulated
}
calculateRating(version) {
return 3.0 + Math.random() * 2; // Simulated
}
getUsageMetrics(version) {
return {
activeUsers: Math.floor(Math.random() * 100),
lastAccessed: new Date().toISOString()
};
}
calculateAdoptionRate(version, allVersions) {
return Math.random(); // Simulated
}
}
//# sourceMappingURL=ListVersionsController.js.map