UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI Productivity Platform

400 lines 16.8 kB
import { BaseToolController } from '../base/BaseToolController.js'; import { ValidationUtils } from '../../utils/ValidationUtils.js'; export class BrowseCategoriesController extends BaseToolController { constructor(ucmClient, logger) { super(ucmClient, logger); } get name() { return 'mcp_ucm_browse_categories'; } get description() { return 'Browse available categories and subcategories in the UCM repository with hierarchical navigation'; } get inputSchema() { return { type: 'object', properties: { author: { type: 'string', description: 'Filter categories by specific author ID', pattern: '^[a-zA-Z0-9\\-_]+$' }, category: { type: 'string', enum: ['commands', 'services', 'patterns', 'implementations', 'contracts', 'guidance'], description: 'Browse subcategories within a specific category' }, includeCounts: { type: 'boolean', default: true, description: 'Include artifact counts for each category/subcategory' }, includePreview: { type: 'boolean', default: false, description: 'Include preview of popular artifacts in each category' }, showEmpty: { type: 'boolean', default: false, description: 'Show categories/subcategories even if they have no artifacts' }, groupBy: { type: 'string', enum: ['category', 'author', 'technology'], default: 'category', description: 'Group results by specified field' }, sortBy: { type: 'string', enum: ['name', 'count', 'recent'], default: 'name', description: 'Sort categories by name, artifact count, or recent activity' } }, required: [] }; } async handleExecute(params) { const { author, category, includeCounts = true, includePreview = false, showEmpty = false, groupBy = 'category', sortBy = 'name' } = params; // Validate inputs if (author) { ValidationUtils.validateAuthorId(author); } if (category) { ValidationUtils.validateCategory(category); } this.logger.debug('BrowseCategoriesController', `Browsing categories, groupBy: ${groupBy}`); try { let categoriesData; if (author) { // Get categories for specific author categoriesData = await this.getAuthorCategories(author); } else { // Get all categories from all authors categoriesData = await this.getAllCategories(); } // Process and structure the data based on groupBy parameter let structuredData; switch (groupBy) { case 'author': structuredData = await this.groupByAuthor(categoriesData, category); break; case 'technology': structuredData = await this.groupByTechnology(categoriesData, category); break; default: // 'category' structuredData = await this.groupByCategory(categoriesData, category); } // Apply filtering and enhancement const processedData = await this.processCategories(structuredData, { includeCounts, includePreview, showEmpty }); // Sort the results const sortedData = this.sortCategories(processedData, sortBy); this.logger.info('BrowseCategoriesController', `Found ${Object.keys(sortedData).length} category groups`); return { categories: sortedData, metadata: { groupBy, sortBy, author, baseCategory: category, includeCounts, includePreview, showEmpty, totalGroups: Object.keys(sortedData).length, timestamp: new Date().toISOString() }, navigation: this.buildNavigationHints(category, author) }; } catch (error) { this.logger.error('BrowseCategoriesController', 'Failed to browse categories', '', error); throw error; } } async getAuthorCategories(authorId) { try { const authorData = await this.ucmClient.getAuthor(authorId); return Array.isArray(authorData) ? authorData : []; } catch (error) { this.logger.warn('BrowseCategoriesController', `Failed to get author ${authorId} data`, '', error); return []; } } async getAllCategories() { try { // Get all authors first const authors = await this.ucmClient.getAuthors(); const allArtifacts = []; // Collect artifacts from all authors for (const author of authors) { try { const authorArtifacts = await this.ucmClient.getAuthor(author.id); if (Array.isArray(authorArtifacts)) { allArtifacts.push(...authorArtifacts); } } catch (error) { // Continue if we can't get data for one author this.logger.debug('BrowseCategoriesController', `Skipping author ${author.id}`, '', error); } } return allArtifacts; } catch (error) { this.logger.error('BrowseCategoriesController', 'Failed to get all categories', '', error); return []; } } async groupByCategory(artifacts, filterCategory) { const categoryGroups = {}; for (const artifact of artifacts) { const category = artifact.metadata?.category || 'uncategorized'; const subcategory = artifact.metadata?.subcategory || 'general'; // Apply category filter if specified if (filterCategory && category !== filterCategory) { continue; } if (!categoryGroups[category]) { categoryGroups[category] = { name: category, description: this.getCategoryDescription(category), subcategories: {}, artifacts: [] }; } if (!categoryGroups[category].subcategories[subcategory]) { categoryGroups[category].subcategories[subcategory] = { name: subcategory, artifacts: [], technologies: new Set(), authors: new Set() }; } categoryGroups[category].subcategories[subcategory].artifacts.push(artifact); if (artifact.metadata?.technology) { categoryGroups[category].subcategories[subcategory].technologies.add(artifact.metadata.technology); } if (artifact.metadata?.author) { categoryGroups[category].subcategories[subcategory].authors.add(artifact.metadata.author); } } // Convert sets to arrays Object.values(categoryGroups).forEach((category) => { Object.values(category.subcategories).forEach((subcategory) => { subcategory.technologies = Array.from(subcategory.technologies); subcategory.authors = Array.from(subcategory.authors); }); }); return categoryGroups; } async groupByAuthor(artifacts, filterCategory) { const authorGroups = {}; for (const artifact of artifacts) { const author = artifact.metadata?.author || 'unknown'; const category = artifact.metadata?.category || 'uncategorized'; // Apply category filter if specified if (filterCategory && category !== filterCategory) { continue; } if (!authorGroups[author]) { authorGroups[author] = { name: author, categories: {}, totalArtifacts: 0 }; } if (!authorGroups[author].categories[category]) { authorGroups[author].categories[category] = { name: category, artifacts: [], subcategories: new Set() }; } authorGroups[author].categories[category].artifacts.push(artifact); if (artifact.metadata?.subcategory) { authorGroups[author].categories[category].subcategories.add(artifact.metadata.subcategory); } authorGroups[author].totalArtifacts++; } // Convert sets to arrays Object.values(authorGroups).forEach((author) => { Object.values(author.categories).forEach((category) => { category.subcategories = Array.from(category.subcategories); }); }); return authorGroups; } async groupByTechnology(artifacts, filterCategory) { const technologyGroups = {}; for (const artifact of artifacts) { const technology = artifact.metadata?.technology || 'technology-agnostic'; const category = artifact.metadata?.category || 'uncategorized'; // Apply category filter if specified if (filterCategory && category !== filterCategory) { continue; } if (!technologyGroups[technology]) { technologyGroups[technology] = { name: technology, categories: {}, totalArtifacts: 0 }; } if (!technologyGroups[technology].categories[category]) { technologyGroups[technology].categories[category] = { name: category, artifacts: [], authors: new Set() }; } technologyGroups[technology].categories[category].artifacts.push(artifact); if (artifact.metadata?.author) { technologyGroups[technology].categories[category].authors.add(artifact.metadata.author); } technologyGroups[technology].totalArtifacts++; } // Convert sets to arrays Object.values(technologyGroups).forEach((tech) => { Object.values(tech.categories).forEach((category) => { category.authors = Array.from(category.authors); }); }); return technologyGroups; } async processCategories(categoriesData, options) { const { includeCounts, includePreview, showEmpty } = options; const processedData = {}; for (const [groupKey, groupData] of Object.entries(categoriesData)) { const processedGroup = { ...groupData }; if (includeCounts) { this.addCounts(processedGroup); } if (includePreview) { this.addPreviews(processedGroup); } // Filter empty categories if showEmpty is false if (!showEmpty) { this.filterEmptyCategories(processedGroup); } // Only add group if it has content or showEmpty is true if (showEmpty || this.hasContent(processedGroup)) { processedData[groupKey] = processedGroup; } } return processedData; } addCounts(groupData) { if (groupData.subcategories) { // Category-based grouping for (const subcategory of Object.values(groupData.subcategories)) { subcategory.count = subcategory.artifacts?.length || 0; } groupData.totalCount = Object.values(groupData.subcategories) .reduce((sum, sub) => sum + (sub.count || 0), 0); } else if (groupData.categories) { // Author or technology-based grouping for (const category of Object.values(groupData.categories)) { category.count = category.artifacts?.length || 0; } groupData.totalCount = groupData.totalArtifacts || 0; } } addPreviews(groupData) { if (groupData.subcategories) { for (const subcategory of Object.values(groupData.subcategories)) { subcategory.preview = this.getPreviewArtifacts(subcategory.artifacts || []); } } else if (groupData.categories) { for (const category of Object.values(groupData.categories)) { category.preview = this.getPreviewArtifacts(category.artifacts || []); } } } filterEmptyCategories(groupData) { if (groupData.subcategories) { for (const [key, subcategory] of Object.entries(groupData.subcategories)) { if (!subcategory.artifacts || subcategory.artifacts.length === 0) { delete groupData.subcategories[key]; } } } else if (groupData.categories) { for (const [key, category] of Object.entries(groupData.categories)) { if (!category.artifacts || category.artifacts.length === 0) { delete groupData.categories[key]; } } } } hasContent(groupData) { if (groupData.subcategories) { return Object.keys(groupData.subcategories).length > 0; } if (groupData.categories) { return Object.keys(groupData.categories).length > 0; } return groupData.totalArtifacts > 0; } getPreviewArtifacts(artifacts) { return artifacts .slice(0, 3) .map(artifact => ({ name: artifact.metadata?.name || 'Unknown', path: artifact.path, description: artifact.metadata?.description?.substring(0, 100) + '...' || '', version: artifact.metadata?.version || '' })); } sortCategories(categoriesData, sortBy) { const sortedEntries = Object.entries(categoriesData).sort(([, a], [, b]) => { switch (sortBy) { case 'count': return (b.totalCount || 0) - (a.totalCount || 0); case 'recent': // Sort by most recent activity (simplified) return 0; // Would implement based on lastUpdated timestamps default: // 'name' return a.name.localeCompare(b.name); } }); return Object.fromEntries(sortedEntries); } getCategoryDescription(category) { const descriptions = { commands: 'Executable micro-block commands that perform specific operations', services: 'Service implementations and interfaces for business logic', patterns: 'Reusable architectural and design patterns', implementations: 'Complete implementations of specific features or systems', contracts: 'Interface contracts and type definitions', guidance: 'Documentation, best practices, and implementation guides' }; return descriptions[category] || 'Miscellaneous artifacts'; } buildNavigationHints(category, author) { const hints = { availableActions: [ 'Use mcp_ucm_get_artifact to view specific artifacts', 'Use mcp_ucm_search_artifacts to search within categories', 'Use mcp_ucm_explore_namespace for detailed browsing' ] }; if (category) { hints.currentLevel = `Category: ${category}`; hints.canNavigateTo = ['subcategories', 'artifacts']; } else { hints.currentLevel = 'All categories'; hints.canNavigateTo = ['specific categories', 'authors', 'technologies']; } if (author) { hints.authorContext = `Filtered by author: ${author}`; } return hints; } } //# sourceMappingURL=BrowseCategoriesController.js.map