Skip to main content
Glama
cbunting99

MCP Code Analysis & Quality Server

by cbunting99
VersionResolver.ts10.6 kB
// Copyright 2025 Chris Bunting // Brief: Handles version constraint resolution and management // Scope: Resolves version conflicts, checks for updates, and manages version constraints import * as semver from 'semver'; import { Logger } from '../utils/Logger.js'; export interface VersionRange { min: string; max: string; includeMin: boolean; includeMax: boolean; } export interface VersionConflict { package: string; versions: string[]; constraints: string[]; resolution?: string; } export interface UpdateInfo { package: string; current: string; latest: string; wanted: string; semverDiff: string; breakingChanges?: BreakingChange[]; } export interface BreakingChange { type: 'major' | 'minor' | 'patch'; description: string; affectedVersion: string; } export class VersionResolver { private logger: Logger; private cache: Map<string, any>; constructor(logger?: Logger) { this.logger = logger || new Logger(); this.cache = new Map(); } async resolveVersionConstraint( packageName: string, constraint: string, availableVersions: string[] ): Promise<string | null> { this.logger.debug(`Resolving version constraint for ${packageName}@${constraint}`); const cacheKey = `${packageName}:${constraint}:${availableVersions.join(',')}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } try { // Filter versions that satisfy the constraint const satisfyingVersions = availableVersions.filter(version => semver.satisfies(version, constraint) ); if (satisfyingVersions.length === 0) { this.logger.warn(`No versions found for ${packageName} satisfying constraint ${constraint}`); return null; } // Sort versions and get the latest one satisfyingVersions.sort((a, b) => semver.compare(b, a)); const resolvedVersion = satisfyingVersions[0]; this.cache.set(cacheKey, resolvedVersion); this.logger.debug(`Resolved ${packageName}@${constraint} to ${resolvedVersion}`); return resolvedVersion; } catch (error) { this.logger.error(`Error resolving version constraint for ${packageName}:`, error); return null; } } async findVersionConflicts( dependencies: Array<{ name: string; version: string; constraint?: string }> ): Promise<VersionConflict[]> { this.logger.debug('Finding version conflicts in dependencies'); const packageMap = new Map<string, Array<{ version: string; constraint?: string }>>(); // Group by package name for (const dep of dependencies) { if (!packageMap.has(dep.name)) { packageMap.set(dep.name, []); } packageMap.get(dep.name)!.push({ version: dep.version, constraint: dep.constraint, }); } const conflicts: VersionConflict[] = []; // Find conflicts (packages with multiple different versions) for (const [packageName, versions] of packageMap) { const uniqueVersions = [...new Set(versions.map(v => v.version))]; if (uniqueVersions.length > 1) { const conflict: VersionConflict = { package: packageName, versions: uniqueVersions, constraints: versions.map(v => v.constraint || '*'), }; // Try to find a resolution conflict.resolution = await this.findConflictResolution(packageName, versions); conflicts.push(conflict); } } this.logger.debug(`Found ${conflicts.length} version conflicts`); return conflicts; } private async findConflictResolution( packageName: string, versions: Array<{ version: string; constraint?: string }> ): Promise<string | undefined> { try { // Try to find a version that satisfies all constraints const constraints = versions.map(v => v.constraint || '*'); const satisfyingVersions = versions.map(v => v.version); // Find the latest version that satisfies all constraints for (const version of satisfyingVersions.sort((a, b) => semver.compare(b, a))) { const satisfiesAll = constraints.every(constraint => semver.satisfies(version, constraint) ); if (satisfiesAll) { return version; } } // If no version satisfies all constraints, try to find the latest compatible version const latestVersion = satisfyingVersions.sort((a, b) => semver.compare(b, a))[0]; return latestVersion; } catch (error) { this.logger.error(`Error finding conflict resolution for ${packageName}:`, error); return undefined; } } async checkForUpdates( packageName: string, currentVersion: string, registryVersions: string[] ): Promise<UpdateInfo | null> { this.logger.debug(`Checking for updates for ${packageName}@${currentVersion}`); try { // Filter valid versions const validVersions = registryVersions.filter(v => semver.valid(v)); validVersions.sort((a, b) => semver.rcompare(a, b)); if (validVersions.length === 0) { return null; } const latest = validVersions[0]; const wanted = this.findLatestCompatible(currentVersion, validVersions); const semverDiff = semver.diff(wanted, currentVersion) || 'none'; const updateInfo: UpdateInfo = { package: packageName, current: currentVersion, latest, wanted, semverDiff, }; // Check for breaking changes if (semverDiff === 'major') { updateInfo.breakingChanges = await this.identifyBreakingChanges( packageName, currentVersion, wanted ); } this.logger.debug(`Update info for ${packageName}: ${JSON.stringify(updateInfo)}`); return updateInfo; } catch (error) { this.logger.error(`Error checking for updates for ${packageName}:`, error); return null; } } private findLatestCompatible(currentVersion: string, availableVersions: string[]): string { const currentRange = semver.validRange(currentVersion) || currentVersion; // Find versions that satisfy the current range const compatibleVersions = availableVersions.filter(v => semver.satisfies(v, currentRange) ); if (compatibleVersions.length === 0) { // If no compatible versions, return the current version return currentVersion; } // Return the latest compatible version return compatibleVersions.sort((a, b) => semver.rcompare(a, b))[0]; } private async identifyBreakingChanges( packageName: string, fromVersion: string, toVersion: string ): Promise<BreakingChange[]> { // This is a simplified implementation // In a real implementation, you would fetch changelog data or use a service // that tracks breaking changes between versions const breakingChanges: BreakingChange[] = []; try { const diff = semver.diff(toVersion, fromVersion); if (diff === 'major') { breakingChanges.push({ type: 'major', description: `Major version update from ${fromVersion} to ${toVersion}`, affectedVersion: toVersion, }); } // Add more sophisticated breaking change detection here // This could involve: // - Fetching and parsing changelogs // - Analyzing API changes // - Checking for deprecated features // - Reviewing type definitions changes } catch (error) { this.logger.error(`Error identifying breaking changes for ${packageName}:`, error); } return breakingChanges; } parseVersionRange(constraint: string): VersionRange | null { try { // Handle various range formats if (constraint.startsWith('^')) { const version = constraint.substring(1); return { min: version, max: semver.inc(version, 'major') || '999.999.999', includeMin: true, includeMax: false, }; } else if (constraint.startsWith('~')) { const version = constraint.substring(1); const parts = version.split('.'); if (parts.length >= 2) { const maxVersion = `${parts[0]}.${parseInt(parts[1]) + 1}.0`; return { min: version, max: maxVersion, includeMin: true, includeMax: false, }; } } else if (constraint.includes(' - ')) { const [min, max] = constraint.split(' - '); return { min: min.trim(), max: max.trim(), includeMin: true, includeMax: true, }; } else if (constraint.includes('>=')) { // Handle >=X.Y.Z format const match = constraint.match(/>=(\d+\.\d+\.\d+)/); if (match) { return { min: match[1], max: '999.999.999', includeMin: true, includeMax: true, }; } } // Default case - exact version if (semver.valid(constraint)) { return { min: constraint, max: constraint, includeMin: true, includeMax: true, }; } return null; } catch (error) { this.logger.error(`Error parsing version range ${constraint}:`, error); return null; } } isVersionInRange(version: string, range: VersionRange): boolean { try { const versionObj = semver.parse(version); if (!versionObj) return false; const minVersion = semver.parse(range.min); const maxVersion = semver.parse(range.max); if (!minVersion || !maxVersion) return false; const minCompare = semver.compare(versionObj, minVersion); const maxCompare = semver.compare(versionObj, maxVersion); const minOk = range.includeMin ? minCompare >= 0 : minCompare > 0; const maxOk = range.includeMax ? maxCompare <= 0 : maxCompare < 0; return minOk && maxOk; } catch (error) { this.logger.error(`Error checking if version ${version} is in range:`, error); return false; } } getUpdatePriority(updateInfo: UpdateInfo): 'high' | 'medium' | 'low' { switch (updateInfo.semverDiff) { case 'major': return 'high'; // Major updates often contain breaking changes case 'minor': return 'medium'; // Minor updates add features case 'patch': return 'low'; // Patch updates are typically bug fixes default: return 'low'; } } clearCache(): void { this.cache.clear(); this.logger.debug('Version resolver cache cleared'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cbunting99/mcp-code-analysis-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server