ucm-mcp-server
Version:
Universal Context Manager MCP Server - AI-native artifact management
465 lines • 19.9 kB
JavaScript
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