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