UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI-native artifact management

200 lines 10.7 kB
import { BaseToolController } from '../base/BaseToolController.js'; import { parsePath } from '../../utils/PathUtils.js'; import { McpError, McpErrorCode } from '../../utils/McpErrorHandler.js'; export class PublishArtifactTool extends BaseToolController { constructor(ucmClient, logger, publishingAuthorId) { super(ucmClient, logger, publishingAuthorId); } get name() { return 'mcp_ucm_publish_artifact'; } get description() { return 'Create or update UCM artifacts with content and metadata. Supports versioning and automatic conflict detection. The API automatically detects whether to create or update based on existing artifacts. NOTE: Only use this tool for small data - use mcp_ucm_publish_artifact_fromfile for files as it is much faster and more efficient.'; } get inputSchema() { return { type: 'object', properties: { path: { type: 'string', description: `Artifact namespace path (e.g., "${this.publishingAuthorId || '1234567890'}/main/commands/user")`, minLength: 1, maxLength: 200 }, filename: { type: 'string', description: 'Filename with extension (e.g., "CreateUserCommand.ts")', minLength: 1, maxLength: 100 }, content: { type: 'string', description: 'The artifact content as text', minLength: 1 }, description: { type: 'string', description: 'Optional description of the artifact' }, version: { type: 'string', description: 'Semantic version (e.g., "1.0.0") - optional, defaults to 1.0.0 for new artifacts' }, technology: { type: 'string', description: 'Technology stack (e.g., "typescript", "python"). Optional - only required for certain categories like "implementations"' }, mimeType: { type: 'string', description: 'MIME type (auto-detected if not provided)' } }, required: ['path', 'filename', 'content'] }; } async handleExecute(params) { const { path, filename, content, description, version, technology, mimeType } = params; this.logger.debug('PublishArtifactTool', `Publishing artifact: ${path}/${filename}`, '', { version: version, size: content.length }); try { // Parse the path to extract components const pathComponents = parsePath(path); // Validate path components exist including repository if (!pathComponents.author || !pathComponents.repository || !pathComponents.category || !pathComponents.subcategory) { throw new McpError(McpErrorCode.InvalidParams, 'Path must contain author, repository, category, and subcategory (e.g., "utaba/main/commands/user")'); } // Validate repository is 'main' for MVP // if (pathComponents.repository !== 'main') { // throw new McpError( // McpErrorCode.InvalidParams, // `Repository must be 'main' for MVP. Received: "${pathComponents.repository}"` // ); // } // Prepare the publish data - new API expects content in body and metadata in query params const publishData = { content, // Raw text content queryParams: { filename, version: version ? version.replace(/^v/, '') : undefined, // Remove v prefix if present, undefined if not provided description: description, technology: technology, mimeType: mimeType || this.detectMimeType(filename) } }; // Always use POST endpoint - the API will detect create vs update automatically const result = await this.ucmClient.publishArtifact(pathComponents.author, pathComponents.repository, pathComponents.category, pathComponents.subcategory, publishData); // Handle response structure - API might return wrapped in 'data' or direct const artifactData = result.data || result; // Build successful response const response = { success: true, artifact: { id: artifactData.id, namespace: `${pathComponents.author}/${pathComponents.repository}/${pathComponents.category}/${pathComponents.subcategory}`, filename, version: publishData.queryParams.version, name: filename, // Use filename as name, matching what the API does description: description, author: pathComponents.author, technology: technology, fileSize: content.length, mimeType: publishData.queryParams.mimeType, createdAt: artifactData.createdAt, publishedAt: artifactData.publishedAt }, links: { self: `/api/v1/authors/${pathComponents.author}/${pathComponents.repository}/${pathComponents.category}/${pathComponents.subcategory}/${filename}`, download: `/api/v1/files/${pathComponents.author}/${pathComponents.repository}/${pathComponents.category}/${pathComponents.subcategory}/${filename}`, versions: `/api/v1/authors/${pathComponents.author}/${pathComponents.repository}/${pathComponents.category}/${pathComponents.subcategory}/${filename}/versions` } }; this.logger.info('PublishArtifactTool', `Artifact published successfully: ${path}/${filename}`, '', { version: publishData.queryParams.version, size: content.length, id: artifactData.id }); return response; } catch (error) { this.logger.error('PublishArtifactTool', `Failed to publish artifact: ${path}/${filename}`, '', error); // Enhanced error handling for publish operations const errorMessage = error instanceof Error ? error.message : String(error); // if (errorMessage?.includes('Version is required')) { // throw new McpError( // McpErrorCode.InvalidParams, // 'Version is required in metadata.version field' // ); // } if (errorMessage?.includes('already exists')) { throw new McpError(McpErrorCode.InvalidParams, 'Artifact version already exists. The API automatically handles updates to existing artifacts.'); } if (errorMessage?.includes('Author not found')) { let authorCustomMessage = ''; if (this.publishingAuthorId) { authorCustomMessage = `Did you mean '${this.publishingAuthorId}'`; } throw new McpError(McpErrorCode.InvalidParams, `Author '${parsePath(path).author}' not found. ${authorCustomMessage}`); } if (errorMessage?.includes('technology') && errorMessage?.includes('required')) { throw new McpError(McpErrorCode.InvalidParams, `This namespace with category "${parsePath(path).category}" requires a technology to be specified. Please provide the technology parameter (e.g., "typescript", "python", "nextjs").`); } // Handle ENOENT errors that might come from the API if (errorMessage?.includes('ENOENT') || errorMessage?.includes('no such file or directory')) { throw new McpError(McpErrorCode.InvalidParams, `Resource not found. This error typically occurs when: 1. A required file or directory is missing 2. The namespace path is incorrect 3. The UCM server cannot access required resources Please verify your input parameters and try again.`); } if (errorMessage?.includes('Invalid namespace format')) { throw new McpError(McpErrorCode.InvalidParams, `Invalid namespace format: "${errorMessage}". The namespace must follow the pattern "${this.publishingAuthorId || 'author'}/repository/category/subcategory" where repository is 'main' (for MVP) and category is one of: commands, services, patterns, implementations, contracts, guidance, project. Example: "${this.publishingAuthorId || '0000000000'}/main/commands/user"`); } throw error; } } validateParams(params) { super.validateParams(params); const { content, version } = params; // Validate version format if provided if (version && version.length > 0) { const versionPattern = /^v?[0-9]+\.[0-9]+\.[0-9]+$/; if (!versionPattern.test(version)) { throw new McpError(McpErrorCode.InvalidParams, 'version must follow semantic versioning format (e.g., "1.0.0" or "v1.0.0")'); } } // Validate content if (!content || typeof content !== 'string') { throw new McpError(McpErrorCode.InvalidParams, 'content is required and must be a string'); } if (content.length > 10 * 1024 * 1024) { // 10MB limit throw new McpError(McpErrorCode.InvalidParams, 'content size exceeds 10MB limit'); } } detectMimeType(filename) { const ext = filename.split('.').pop()?.toLowerCase(); const mimeTypes = { 'ts': 'application/typescript', 'js': 'application/javascript', 'json': 'application/json', 'md': 'text/markdown', 'txt': 'text/plain', 'yaml': 'text/yaml', 'yml': 'text/yaml', 'html': 'text/html', 'css': 'text/css', 'py': 'text/x-python', 'java': 'text/x-java', 'cs': 'text/x-csharp', 'go': 'text/x-go', 'rs': 'text/x-rust', 'cpp': 'text/x-c++', 'c': 'text/x-c', 'sh': 'text/x-shellscript', 'xml': 'application/xml' }; return mimeTypes[ext || ''] || 'text/plain'; } } //# sourceMappingURL=PublishArtifactTool.js.map