UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI-native artifact management

605 lines 26 kB
import { BaseToolController } from '../base/BaseToolController.js'; import { ValidationUtils } from '../../utils/ValidationUtils.js'; import { parsePath } from '../../utils/PathUtils.js'; export class PublishArtifactController extends BaseToolController { constructor(ucmClient, logger) { super(ucmClient, logger); } get name() { return 'mcp_ucm_publish_artifact'; } get description() { return 'Publish a new artifact or version to the UCM repository with comprehensive validation and metadata'; } get inputSchema() { return { type: 'object', properties: { path: { type: 'string', description: 'Target artifact path (e.g., "author/category/subcategory/technology/version" or "author/category/subcategory/version")', minLength: 10, maxLength: 200 }, content: { type: 'string', description: 'Artifact source content (code, documentation, etc.)', minLength: 1, maxLength: 1048576 // 1MB limit }, metadata: { type: 'object', properties: { name: { type: 'string', description: 'Human-readable name for the artifact', minLength: 1, maxLength: 100 }, description: { type: 'string', description: 'Detailed description of the artifact', minLength: 10, maxLength: 2000 }, version: { type: 'string', pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[a-zA-Z0-9\\-_]+)?$', description: 'Semantic version (e.g., "1.0.0", "2.1.0-beta")' }, contractVersion: { type: 'string', pattern: '^[0-9]+\\.[0-9]+$', description: 'API contract version (e.g., "1.0")' }, dependencies: { type: 'object', properties: { services: { type: 'array', items: { type: 'string' }, description: 'Required service interfaces' }, commands: { type: 'array', items: { type: 'string' }, description: 'Required command dependencies' }, external: { type: 'object', additionalProperties: { type: 'string' }, description: 'External package dependencies' } } }, tags: { type: 'array', items: { type: 'string', pattern: '^[a-zA-Z0-9\\-_]+$' }, maxItems: 20, description: 'Searchable tags for categorization' }, license: { type: 'string', description: 'License identifier (e.g., "MIT", "Apache-2.0")' }, maintainers: { type: 'array', items: { type: 'string' }, description: 'List of maintainer IDs' } }, required: ['name', 'description', 'version'] }, examples: { type: 'array', items: { type: 'object', properties: { title: { type: 'string' }, description: { type: 'string' }, code: { type: 'string' }, language: { type: 'string' }, complexity: { type: 'string', enum: ['beginner', 'intermediate', 'advanced'] }, runnable: { type: 'boolean', default: true } }, required: ['title', 'code'] }, description: 'Usage examples and documentation' }, publishOptions: { type: 'object', properties: { validateContent: { type: 'boolean', default: true, description: 'Perform content validation before publishing' }, generateDocs: { type: 'boolean', default: false, description: 'Auto-generate documentation from content' }, notifyMaintainers: { type: 'boolean', default: false, description: 'Notify existing maintainers of new version' }, replaceExisting: { type: 'boolean', default: false, description: 'Replace existing version if it exists (dangerous)' }, draft: { type: 'boolean', default: false, description: 'Publish as draft (not publicly visible)' } } } }, required: ['path', 'content', 'metadata'] }; } async handleExecute(params) { const { path, content, metadata, examples = [], publishOptions = {} } = params; // Validate inputs await this.validatePublishRequest(path, content, metadata, examples, publishOptions); this.logger.debug('PublishArtifactController', `Publishing artifact to: ${path}`); try { // Pre-publish validation and preparation const validationResult = await this.performPrePublishValidation(path, content, metadata, examples, publishOptions); if (!validationResult.valid) { throw this.formatError(new Error(`Validation failed: ${validationResult.errors.join(', ')}`)); } // Check for existing artifact and handle conflicts await this.handleExistingArtifact(path, publishOptions); // Prepare publish data const publishData = await this.preparePublishData(content, metadata, examples, publishOptions); // Parse the path to get component parts const parsed = parsePath(path); if (!parsed.author || !parsed.category || !parsed.subcategory || !parsed.filename) { throw this.formatError(new Error(`Validation failed: You must supply author,category,subcategory,filename. 1 or more of these parameters are missing.`)); } // Publish to UCM API const publishResult = await this.ucmClient.publishArtifact(parsed.author, parsed.category, parsed.subcategory, publishData); // Post-publish processing const postProcessResult = await this.performPostPublishProcessing(publishResult, publishOptions); const response = { success: true, artifact: { id: publishResult.id, path: publishResult.path, name: metadata.name, version: metadata.version, author: metadata.author || this.extractAuthorFromPath(path), category: metadata.category || this.extractCategoryFromPath(path), publishedAt: publishResult.publishedAt || new Date().toISOString() }, validation: validationResult, publishing: { method: publishOptions.replaceExisting ? 'replaced' : 'created', draft: publishOptions.draft || false, contentSize: content.length, examplesCount: examples.length }, postProcessing: postProcessResult, urls: { view: this.buildViewUrl(publishResult.path), download: this.buildDownloadUrl(publishResult.path), api: this.buildApiUrl(publishResult.path) }, metadata: { timestamp: new Date().toISOString(), clientInfo: 'UCM-MCP-Server', publishDurationMs: 0 // Would be measured in real implementation } }; this.logger.info('PublishArtifactController', `Successfully published artifact: ${path}`); return response; } catch (error) { this.logger.error('PublishArtifactController', `Failed to publish artifact: ${path}`, '', error); throw error; } } async validatePublishRequest(path, content, metadata, examples, publishOptions) { // Validate path format ValidationUtils.validateArtifactPath(path); // Validate content size ValidationUtils.validateContentSize(content); // Validate metadata fields if (!metadata.name || typeof metadata.name !== 'string') { throw this.formatError(new Error('Metadata name is required')); } if (!metadata.description || metadata.description.length < 10) { throw this.formatError(new Error('Metadata description must be at least 10 characters')); } if (!metadata.version) { throw this.formatError(new Error('Metadata version is required')); } ValidationUtils.validateVersion(metadata.version); // Validate path components match metadata const pathComponents = this.parseArtifactPath(path); if (metadata.author && metadata.author !== pathComponents.author) { throw this.formatError(new Error('Metadata author must match path author')); } if (metadata.category && metadata.category !== pathComponents.category) { throw this.formatError(new Error('Metadata category must match path category')); } // Validate examples if provided if (examples && examples.length > 0) { for (let i = 0; i < examples.length; i++) { const example = examples[i]; if (!example.title || !example.code) { throw this.formatError(new Error(`Example ${i + 1} missing required title or code`)); } } } // Validate tags if provided if (metadata.tags) { for (const tag of metadata.tags) { if (typeof tag !== 'string' || !tag.match(/^[a-zA-Z0-9\-_]+$/)) { throw this.formatError(new Error(`Invalid tag format: ${tag}`)); } } } } async performPrePublishValidation(path, content, metadata, examples, publishOptions) { const validationResult = { valid: true, errors: [], warnings: [], suggestions: [], contentAnalysis: {}, securityScan: {} }; // Content validation if (publishOptions.validateContent !== false) { const contentValidation = await this.validateContent(content, metadata); validationResult.contentAnalysis = contentValidation; if (!contentValidation.valid) { validationResult.valid = false; validationResult.errors.push(...contentValidation.errors); } validationResult.warnings.push(...contentValidation.warnings); validationResult.suggestions.push(...contentValidation.suggestions); } // Security validation const securityScan = await this.performSecurityScan(content); validationResult.securityScan = securityScan; if (securityScan.hasVulnerabilities) { validationResult.valid = false; validationResult.errors.push(...securityScan.vulnerabilities); } // Dependency validation if (metadata.dependencies) { const depValidation = await this.validateDependencies(metadata.dependencies); if (!depValidation.valid) { validationResult.warnings.push(...depValidation.warnings); } } // Quality checks const qualityChecks = this.performQualityChecks(metadata, content, examples); validationResult.warnings.push(...qualityChecks.warnings); validationResult.suggestions.push(...qualityChecks.suggestions); return validationResult; } async handleExistingArtifact(path, publishOptions) { try { const parsed = parsePath(path); if (!parsed.filename) { throw new Error('Filename is required to check existing artifact'); } const existingArtifact = await this.ucmClient.getArtifact(parsed.author, parsed.category, parsed.subcategory, parsed.filename, parsed.version); if (existingArtifact && !publishOptions.replaceExisting) { throw this.formatError(new Error(`Artifact already exists at path: ${path}. Use replaceExisting option to overwrite.`)); } if (existingArtifact && publishOptions.replaceExisting) { this.logger.warn('PublishArtifactController', `Replacing existing artifact at: ${path}`); } } catch (error) { // If artifact doesn't exist, that's what we want for new publications if (error instanceof Error && error.message?.includes('not found')) { return; // This is expected for new artifacts } throw error; } } async preparePublishData(content, metadata, examples, publishOptions) { const publishData = { content, metadata: { ...metadata, // Add computed metadata contentHash: this.calculateContentHash(content), contentType: this.detectContentType(content), publishedBy: 'MCP-Server', // In real implementation, would use authenticated user publishedAt: new Date().toISOString(), draft: publishOptions.draft || false } }; // Add examples if provided if (examples && examples.length > 0) { publishData.examples = examples.map((example, index) => ({ ...example, id: index + 1, language: example.language || this.detectLanguageFromContent(example.code) })); } // Generate documentation if requested if (publishOptions.generateDocs) { publishData.generatedDocs = await this.generateDocumentation(content, metadata); } return publishData; } async performPostPublishProcessing(publishResult, publishOptions) { const postProcessing = { actions: [], notifications: [], indexing: { searchIndexed: false, cacheUpdated: false }, analytics: { recorded: false } }; // Index for search try { await this.indexForSearch(publishResult); postProcessing.indexing.searchIndexed = true; postProcessing.actions.push('Indexed for search'); } catch (error) { this.logger.warn('PublishArtifactController', 'Failed to index for search', '', error); } // Update caches try { await this.updateCaches(publishResult); postProcessing.indexing.cacheUpdated = true; postProcessing.actions.push('Updated caches'); } catch (error) { this.logger.warn('PublishArtifactController', 'Failed to update caches', '', error); } // Send notifications if requested if (publishOptions.notifyMaintainers) { try { const notifications = await this.sendMaintainerNotifications(publishResult); postProcessing.notifications = notifications; postProcessing.actions.push('Notified maintainers'); } catch (error) { this.logger.warn('PublishArtifactController', 'Failed to send notifications', '', error); } } // Record analytics try { await this.recordPublishAnalytics(publishResult); postProcessing.analytics.recorded = true; postProcessing.actions.push('Recorded analytics'); } catch (error) { this.logger.warn('PublishArtifactController', 'Failed to record analytics', '', error); } return postProcessing; } // Validation helper methods async validateContent(content, metadata) { const validation = { valid: true, errors: [], warnings: [], suggestions: [] }; // Check content type consistency const detectedType = this.detectContentType(content); const pathType = this.extractTechnologyFromMetadata(metadata); if (pathType && detectedType !== pathType && detectedType !== 'text') { validation.warnings.push(`Content type (${detectedType}) may not match expected type (${pathType})`); } // Check for potential issues if (content.includes('TODO') || content.includes('FIXME')) { validation.warnings.push('Content contains TODO or FIXME comments'); } if (content.length < 100) { validation.suggestions.push('Consider adding more comprehensive implementation'); } // Syntax validation for code content if (this.isCodeContent(detectedType)) { const syntaxValidation = this.validateSyntax(content, detectedType); if (!syntaxValidation.valid) { validation.errors.push(...syntaxValidation.errors); validation.valid = false; } } return validation; } async performSecurityScan(content) { const scan = { hasVulnerabilities: false, vulnerabilities: [], warnings: [], score: 100 }; // Check for potential security issues const securityPatterns = [ { pattern: /password\s*=\s*['"]/i, message: 'Hardcoded password detected' }, { pattern: /api[_-]?key\s*=\s*['"]/i, message: 'Hardcoded API key detected' }, { pattern: /secret\s*=\s*['"]/i, message: 'Hardcoded secret detected' }, { pattern: /eval\s*\(/i, message: 'Use of eval() function detected' }, { pattern: /innerHTML\s*=/i, message: 'Potential XSS vulnerability with innerHTML' } ]; for (const { pattern, message } of securityPatterns) { if (pattern.test(content)) { scan.hasVulnerabilities = true; scan.vulnerabilities.push(message); scan.score -= 20; } } return scan; } async validateDependencies(dependencies) { const validation = { valid: true, warnings: [] }; // Check if external dependencies are reasonable if (dependencies.external) { const externalCount = Object.keys(dependencies.external).length; if (externalCount > 10) { validation.warnings.push(`High number of external dependencies (${externalCount})`); } } return validation; } performQualityChecks(metadata, content, examples) { const checks = { warnings: [], suggestions: [] }; // Check metadata completeness if (!metadata.tags || metadata.tags.length === 0) { checks.suggestions.push('Consider adding tags for better discoverability'); } if (!metadata.dependencies) { checks.suggestions.push('Consider documenting dependencies if any exist'); } // Check for examples if (!examples || examples.length === 0) { checks.suggestions.push('Consider adding usage examples'); } // Check content quality if (content.length < 500) { checks.suggestions.push('Consider providing more detailed implementation'); } return checks; } // Helper methods parseArtifactPath(path) { const parts = path.split('/'); if (parts.length === 4) { // author/category/subcategory/version (technology-agnostic) return { author: parts[0], category: parts[1], subcategory: parts[2], technology: null, version: parts[3] }; } else if (parts.length === 5) { // author/category/subcategory/technology/version return { author: parts[0], category: parts[1], subcategory: parts[2], technology: parts[3], version: parts[4] }; } throw new Error('Invalid artifact path format'); } extractAuthorFromPath(path) { return path.split('/')[0]; } extractCategoryFromPath(path) { return path.split('/')[1]; } extractTechnologyFromMetadata(metadata) { return metadata.technology || null; } calculateContentHash(content) { // Simple hash calculation (in real implementation, would use crypto) let hash = 0; for (let i = 0; i < content.length; i++) { const char = content.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(16); } detectContentType(content) { if (content.includes('interface ') || content.includes('export class')) return 'typescript'; if (content.includes('function ') || content.includes('const ')) return 'javascript'; if (content.includes('def ') || content.includes('import ')) return 'python'; if (content.includes('```') || content.includes('# ')) return 'markdown'; return 'text'; } detectLanguageFromContent(code) { return this.detectContentType(code); } isCodeContent(contentType) { return ['typescript', 'javascript', 'python', 'java', 'cpp'].includes(contentType); } validateSyntax(content, contentType) { // Simplified syntax validation const validation = { valid: true, errors: [] }; // Basic bracket matching const brackets = { '(': ')', '[': ']', '{': '}' }; const stack = []; for (const char of content) { if (char in brackets) { stack.push(brackets[char]); } else if (Object.values(brackets).includes(char)) { if (stack.length === 0 || stack.pop() !== char) { validation.valid = false; validation.errors.push('Mismatched brackets detected'); break; } } } if (stack.length > 0) { validation.valid = false; validation.errors.push('Unclosed brackets detected'); } return validation; } async generateDocumentation(content, metadata) { // Simplified documentation generation return { generated: true, sections: ['overview', 'usage', 'api'], content: `# ${metadata.name}\n\n${metadata.description}\n\n## Usage\n\nSee examples for usage instructions.` }; } buildViewUrl(path) { return `/browse/artifacts/${path}`; } buildDownloadUrl(path) { return `/api/v1/authors/${path}/download`; } buildApiUrl(path) { return `/api/v1/authors/${path}`; } // Post-processing methods (simplified for demo) async indexForSearch(publishResult) { // Would integrate with search indexing system } async updateCaches(publishResult) { // Would update various caches } async sendMaintainerNotifications(publishResult) { // Would send notifications to maintainers return ['Notification sent to maintainers']; } async recordPublishAnalytics(publishResult) { // Would record analytics data } } //# sourceMappingURL=PublishArtifactController.js.map