UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI Productivity Platform

267 lines 14.3 kB
import { BaseToolController } from '../base/BaseToolController.js'; import { parsePath } from '../../utils/PathUtils.js'; import { McpError, McpErrorCode } from '../../utils/McpErrorHandler.js'; import { readFileSync, existsSync, statSync } from 'fs'; import { fileURLToPath } from 'url'; export class PublishArtifactFromFileTool extends BaseToolController { constructor(ucmClient, logger, publishingAuthorId) { super(ucmClient, logger, publishingAuthorId); } get name() { return 'ucm_publish_artifact_fromfile'; } get description() { return 'Create or update UCM artifacts from a file URI. Supports versioning. PREFERRED METHOD: Use this tool for file-based content as it is much faster and more efficient. Pass the local file path as a file:// URI (e.g., "file:///path/to/file.txt").'; } 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 }, fileUri: { type: 'string', description: 'File URI pointing to the content file (e.g., "file:///tmp/large-file.txt"). Use local file paths converted to file:// URIs.', 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 and will attempt to auto-detect' }, mimeType: { type: 'string', description: 'MIME type (auto-detected if not provided)' }, tags: { type: 'array', description: 'Optional array of tags for categorization', items: { type: 'string' } } }, required: ['path', 'filename', 'fileUri'] }; } async handleExecute(params) { const { path, filename, fileUri, description, version, technology, mimeType, tags } = params; this.logger.debug('PublishArtifactFromFileTool', `Publishing artifact from file: ${fileUri}`, '', { path: `${path}/${filename}`, version: version }); try { // Parse and validate the file URI const filePath = this.parseFileUri(fileUri); // Read the file content const content = this.readFileContent(filePath); this.logger.debug('PublishArtifactFromFileTool', `File read successfully: ${filePath}`, '', { size: content.length }); // 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., "${this.publishingAuthorId || '1234567890'}/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 from file 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), tags: tags } }; // 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, sourceFile: filePath }, 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('PublishArtifactFromFileTool', `Artifact published successfully from file: ${filePath}`, '', { path: `${path}/${filename}`, version: publishData.queryParams.version, size: content.length, id: artifactData.id }); return response; } catch (error) { this.logger.error('PublishArtifactFromFileTool', `Failed to publish artifact from file: ${fileUri}`, '', error); // Enhanced error handling for publish operations const errorMessage = error instanceof Error ? error.message : String(error); // Handle ENOENT errors that might come from the API or filesystem if (errorMessage?.includes('ENOENT') || errorMessage?.includes('no such file or directory')) { throw new McpError(McpErrorCode.InvalidParams, `File not found: The specified file "${fileUri}" does not exist. Please verify: 1. The file path is correct and complete 2. The file exists at the specified location 3. You have permission to access the file 4. The file URI uses the correct format: file:///full/path/to/file.ext`); } 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").`); } if (errorMessage?.includes('Invalid namespace format')) { throw new McpError(McpErrorCode.InvalidParams, `Invalid namespace format: "${path}". 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: "utaba/main/commands/user"`); } throw error; } } validateParams(params) { super.validateParams(params); const { fileUri, version } = params; // Validate file URI if (!fileUri || typeof fileUri !== 'string') { throw new McpError(McpErrorCode.InvalidParams, 'fileUri is required and must be a string'); } // Validate file URI format if (!fileUri.startsWith('file://')) { throw new McpError(McpErrorCode.InvalidParams, 'fileUri must be a valid file URI starting with "file://"'); } // 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")'); } } } parseFileUri(fileUri) { try { // Convert file URI to file path const filePath = fileURLToPath(fileUri); // Security check - ensure file is accessible and not attempting path traversal if (filePath.includes('..') || filePath.includes('~')) { throw new McpError(McpErrorCode.InvalidParams, 'File path contains invalid characters or path traversal attempts'); } return filePath; } catch (error) { throw new McpError(McpErrorCode.InvalidParams, `Invalid file URI: ${fileUri}. Must be a valid file:// URI.`); } } readFileContent(filePath) { try { // Check if file exists if (!existsSync(filePath)) { throw new McpError(McpErrorCode.InvalidParams, `File not found: ${filePath}. Please verify the file path is correct and the file exists.`); } // Check file size (maintain reasonable limit for memory usage) const stats = statSync(filePath); const maxFileSize = 100 * 1024 * 1024; // 100MB limit if (stats.size > maxFileSize) { throw new McpError(McpErrorCode.InvalidParams, `File size (${stats.size} bytes) exceeds maximum limit of ${maxFileSize} bytes`); } // Read file content const content = readFileSync(filePath, 'utf8'); return content; } catch (error) { if (error instanceof McpError) { throw error; } // Handle specific filesystem errors with AI-friendly messages if (error instanceof Error) { if (error.message.includes('ENOENT') || error.message.includes('no such file or directory')) { throw new McpError(McpErrorCode.InvalidParams, `File not found: ${filePath}. The file does not exist at the specified path. Please check that: 1. The file path is correct 2. The file has not been moved or deleted 3. You have permission to access the file 4. The path uses forward slashes (/), not backslashes (\\)`); } if (error.message.includes('EACCES') || error.message.includes('permission denied')) { throw new McpError(McpErrorCode.InvalidParams, `Permission denied: Cannot read file ${filePath}. Please check file permissions.`); } if (error.message.includes('EISDIR') || error.message.includes('illegal operation on a directory')) { throw new McpError(McpErrorCode.InvalidParams, `Invalid file path: ${filePath} is a directory, not a file. Please specify the full path to a file.`); } } throw new McpError(McpErrorCode.InternalError, `Failed to read file: ${error instanceof Error ? error.message : String(error)}`); } } 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=PublishArtifactFromFileTool.js.map