@utaba/ucm-mcp-server
Version:
Universal Context Manager MCP Server - AI Productivity Platform
400 lines • 16.8 kB
JavaScript
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