@utaba/ucm-mcp-server
Version:
Universal Context Manager MCP Server - AI-native artifact management
605 lines • 26 kB
JavaScript
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