UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI-native artifact management

548 lines 23.7 kB
import { BaseToolController } from '../base/BaseToolController.js'; import { ValidationUtils } from '../../utils/ValidationUtils.js'; export class ExploreNamespaceController extends BaseToolController { constructor(ucmClient, logger) { super(ucmClient, logger); } get name() { return 'mcp_ucm_explore_namespace'; } get description() { return 'Explore the hierarchical namespace structure of UCM artifacts with interactive navigation'; } get inputSchema() { return { type: 'object', properties: { path: { type: 'string', description: 'Namespace path to explore (e.g., "utaba", "utaba/commands", "utaba/commands/user"). Leave empty for root exploration.', default: '', maxLength: 150 }, depth: { type: 'number', minimum: 1, maximum: 5, default: 2, description: 'How deep to explore the namespace hierarchy' }, includeContent: { type: 'boolean', default: false, description: 'Include content previews for artifacts found' }, includeStats: { type: 'boolean', default: true, description: 'Include statistics for each namespace level' }, sortBy: { type: 'string', enum: ['name', 'count', 'recent', 'popular'], default: 'name', description: 'Sort namespaces by specified criteria' }, filterBy: { type: 'object', properties: { technology: { type: 'string', description: 'Filter by technology (e.g., "typescript", "python")' }, category: { type: 'string', enum: ['commands', 'services', 'patterns', 'implementations', 'contracts', 'guidance'], description: 'Filter by artifact category' }, hasArtifacts: { type: 'boolean', description: 'Only show namespaces that contain artifacts' }, updatedSince: { type: 'string', format: 'date', description: 'Only show namespaces with artifacts updated since this date' } } }, expandPaths: { type: 'array', items: { type: 'string' }, description: 'Specific paths to expand regardless of depth limit' }, includeNavigationHints: { type: 'boolean', default: true, description: 'Include hints for further navigation' } }, required: [] }; } async handleExecute(params) { const { path = '', depth = 2, includeContent = false, includeStats = true, sortBy = 'name', filterBy = {}, expandPaths = [], includeNavigationHints = true } = params; // Validate inputs if (path && !this.isValidNamespacePath(path)) { throw this.formatError(new Error('Invalid namespace path format')); } if (filterBy.category) { ValidationUtils.validateCategory(filterBy.category); } this.logger.debug('ExploreNamespaceController', `Exploring namespace: "${path}" with depth ${depth}`); try { // Start exploration from the specified path const explorationResult = await this.exploreNamespace(path, depth, { includeContent, includeStats, sortBy, filterBy, expandPaths }); // Build comprehensive response const response = { exploration: explorationResult, currentPath: path, explorationOptions: { depth, includeContent, includeStats, sortBy, appliedFilters: filterBy }, navigation: includeNavigationHints ? this.generateNavigationHints(explorationResult) : null, breadcrumbs: this.buildBreadcrumbs(path), metadata: { timestamp: new Date().toISOString(), totalNodesExplored: this.countTotalNodes(explorationResult), maxDepthReached: this.calculateMaxDepth(explorationResult), hasMoreContent: this.hasMoreContent(explorationResult, depth) } }; this.logger.info('ExploreNamespaceController', `Explored namespace with ${response.metadata.totalNodesExplored} nodes`); return response; } catch (error) { this.logger.error('ExploreNamespaceController', `Failed to explore namespace: ${path}`, '', error); throw error; } } async exploreNamespace(path, remainingDepth, options) { if (remainingDepth <= 0 && !options.expandPaths.includes(path)) { return null; } const node = { path, name: this.getNameFromPath(path), type: this.determineNodeType(path), level: this.calculateLevel(path), children: [], artifacts: [] }; try { // Get direct content at this level const directContent = await this.getDirectContent(path, options.filterBy); // Process artifacts at this level if (directContent.artifacts && directContent.artifacts.length > 0) { node.artifacts = await this.processArtifacts(directContent.artifacts, options); } // Process child namespaces if (directContent.children && directContent.children.length > 0) { const childPromises = directContent.children.map(async (childPath) => { const shouldExpand = remainingDepth > 1 || options.expandPaths.includes(childPath); if (shouldExpand) { return await this.exploreNamespace(childPath, remainingDepth - 1, options); } else { // Return summary info for unexplored children return { path: childPath, name: this.getNameFromPath(childPath), type: this.determineNodeType(childPath), level: this.calculateLevel(childPath), summary: await this.getNodeSummary(childPath, options.filterBy), children: null, // Indicates unexplored artifacts: [] }; } }); const childResults = await Promise.all(childPromises); node.children = childResults.filter(child => child !== null); } // Add statistics if requested if (options.includeStats) { node.statistics = await this.calculateNodeStatistics(node, options.filterBy); } // Sort children based on sort criteria if (node.children && node.children.length > 0) { node.children = this.sortNodes(node.children, options.sortBy); } return node; } catch (error) { this.logger.warn('ExploreNamespaceController', `Failed to explore path: ${path}`, '', error); return { path, name: this.getNameFromPath(path), type: 'error', level: this.calculateLevel(path), error: error instanceof Error ? error.message : String(error), children: [], artifacts: [] }; } } async getDirectContent(path, filterBy) { const content = { artifacts: [], children: [] }; try { if (!path) { // Root level - get all authors const authors = await this.ucmClient.getAuthors(); content.children = authors.map((author) => author.id); } else { const pathParts = path.split('/'); if (pathParts.length === 1) { // Author level - get categories const authorData = await this.ucmClient.getAuthor(pathParts[0]); if (Array.isArray(authorData)) { // Extract unique categories const categories = new Set(); authorData.forEach((artifact) => { if (artifact.metadata?.category) { categories.add(artifact.metadata.category); } }); content.children = Array.from(categories).map(cat => `${path}/${cat}`); } } else if (pathParts.length === 2) { // Category level - get subcategories and artifacts const [author, category] = pathParts; const authorData = await this.ucmClient.getAuthor(author); if (Array.isArray(authorData)) { const categoryArtifacts = authorData.filter((artifact) => artifact.metadata?.category === category); // Filter artifacts const filteredArtifacts = this.applyFilters(categoryArtifacts, filterBy); content.artifacts = filteredArtifacts; // Extract subcategories const subcategories = new Set(); categoryArtifacts.forEach((artifact) => { if (artifact.metadata?.subcategory) { subcategories.add(artifact.metadata.subcategory); } }); content.children = Array.from(subcategories).map(sub => `${path}/${sub}`); } } else { // Deeper levels - get artifacts const artifacts = await this.getArtifactsAtPath(path); content.artifacts = this.applyFilters(artifacts, filterBy); } } } catch (error) { // If we can't get content, return empty but don't fail this.logger.debug('ExploreNamespaceController', `No content found at path: ${path}`, '', error); } return content; } async getArtifactsAtPath(path) { try { // Try to get artifacts at this specific path const pathParts = path.split('/'); if (pathParts.length >= 3) { const [author, category, subcategory] = pathParts; const authorData = await this.ucmClient.getAuthor(author); if (Array.isArray(authorData)) { return authorData.filter((artifact) => artifact.metadata?.category === category && artifact.metadata?.subcategory === subcategory); } } } catch (error) { this.logger.debug('ExploreNamespaceController', `No artifacts at path: ${path}`, '', error); } return []; } applyFilters(artifacts, filterBy) { let filtered = artifacts; if (filterBy.technology) { filtered = filtered.filter(artifact => artifact.metadata?.technology === filterBy.technology); } if (filterBy.category) { filtered = filtered.filter(artifact => artifact.metadata?.category === filterBy.category); } if (filterBy.updatedSince) { const sinceDate = new Date(filterBy.updatedSince); filtered = filtered.filter(artifact => { const updated = new Date(artifact.lastUpdated || artifact.publishedAt || 0); return updated > sinceDate; }); } return filtered; } async processArtifacts(artifacts, options) { return artifacts.map(artifact => { const processed = { id: artifact.id, name: artifact.metadata?.name || 'Unknown', path: artifact.path, version: artifact.metadata?.version || 'unknown', technology: artifact.metadata?.technology || null, description: artifact.metadata?.description || '', lastUpdated: artifact.lastUpdated, tags: artifact.metadata?.tags || [] }; // Include content preview if requested if (options.includeContent && artifact.content) { processed.contentPreview = this.generateContentPreview(artifact.content); } return processed; }); } async getNodeSummary(path, filterBy) { try { const content = await this.getDirectContent(path, filterBy); return { artifactCount: content.artifacts?.length || 0, childCount: content.children?.length || 0, hasContent: (content.artifacts?.length || 0) > 0 || (content.children?.length || 0) > 0, lastActivity: this.getLastActivity(content.artifacts || []) }; } catch (error) { return { artifactCount: 0, childCount: 0, hasContent: false, lastActivity: null, error: error instanceof Error ? error.message : String(error) }; } } async calculateNodeStatistics(node, filterBy) { const stats = { directArtifacts: node.artifacts?.length || 0, totalArtifacts: 0, totalChildren: node.children?.length || 0, depth: node.level, technologies: new Set(), categories: new Set(), authors: new Set(), lastActivity: null, popularityScore: 0 }; // Count total artifacts recursively stats.totalArtifacts = this.countArtifactsRecursively(node); // Collect technology and category information this.collectMetadataRecursively(node, stats); // Calculate last activity stats.lastActivity = this.getLastActivityRecursively(node); // Calculate popularity score stats.popularityScore = this.calculatePopularityScore(node); return { ...stats, technologies: Array.from(stats.technologies), categories: Array.from(stats.categories), authors: Array.from(stats.authors) }; } sortNodes(nodes, sortBy) { return nodes.sort((a, b) => { switch (sortBy) { case 'count': const aCount = (a.statistics?.totalArtifacts || a.summary?.artifactCount || 0); const bCount = (b.statistics?.totalArtifacts || b.summary?.artifactCount || 0); return bCount - aCount; case 'recent': const aActivity = a.statistics?.lastActivity || a.summary?.lastActivity || '1970-01-01'; const bActivity = b.statistics?.lastActivity || b.summary?.lastActivity || '1970-01-01'; return new Date(bActivity).getTime() - new Date(aActivity).getTime(); case 'popular': const aPopularity = a.statistics?.popularityScore || 0; const bPopularity = b.statistics?.popularityScore || 0; return bPopularity - aPopularity; default: // 'name' return a.name.localeCompare(b.name); } }); } generateNavigationHints(explorationResult) { const hints = { nextSteps: [], interestingPaths: [], recommendations: [], shortcuts: [] }; // Analyze exploration result to generate helpful hints if (explorationResult.children && explorationResult.children.length > 0) { const unexplored = explorationResult.children.filter((child) => child.children === null); if (unexplored.length > 0) { hints.nextSteps.push(`Explore ${unexplored.length} unexplored child namespaces`); hints.shortcuts.push('Use higher depth parameter to explore deeper automatically'); } } // Find interesting paths based on artifact counts this.findInterestingPaths(explorationResult, hints.interestingPaths); // Generate recommendations based on content if (explorationResult.statistics) { const stats = explorationResult.statistics; if (stats.totalArtifacts > 10) { hints.recommendations.push('This namespace has rich content - consider using search tools for specific artifacts'); } if (stats.technologies.length > 3) { hints.recommendations.push('Multiple technologies available - filter by technology for focused exploration'); } } return hints; } buildBreadcrumbs(path) { if (!path) { return [{ name: 'Root', path: '', level: 0 }]; } const parts = path.split('/'); const breadcrumbs = [{ name: 'Root', path: '', level: 0 }]; let currentPath = ''; parts.forEach((part, index) => { currentPath = currentPath ? `${currentPath}/${part}` : part; breadcrumbs.push({ name: part, path: currentPath, level: index + 1 }); }); return breadcrumbs; } // Helper methods isValidNamespacePath(path) { if (!path) return true; // Empty path is valid (root) return /^[a-zA-Z0-9\-_]+(\/[a-zA-Z0-9\-_]+)*$/.test(path); } getNameFromPath(path) { if (!path) return 'Root'; const parts = path.split('/'); return parts[parts.length - 1]; } determineNodeType(path) { if (!path) return 'root'; const parts = path.split('/'); switch (parts.length) { case 1: return 'author'; case 2: return 'category'; case 3: return 'subcategory'; case 4: return 'technology-or-version'; default: return 'artifact'; } } calculateLevel(path) { return path ? path.split('/').length : 0; } generateContentPreview(content) { const lines = content.split('\n'); const preview = lines.slice(0, 5).join('\n'); return preview.length > 200 ? preview.substring(0, 200) + '...' : preview; } getLastActivity(artifacts) { if (!artifacts || artifacts.length === 0) return null; const dates = artifacts .map(artifact => artifact.lastUpdated || artifact.publishedAt) .filter(date => date) .sort((a, b) => new Date(b).getTime() - new Date(a).getTime()); return dates.length > 0 ? dates[0] : null; } countTotalNodes(node) { if (!node) return 0; let count = 1; // Count this node if (node.children) { count += node.children.reduce((sum, child) => sum + this.countTotalNodes(child), 0); } return count; } calculateMaxDepth(node) { if (!node || !node.children || node.children.length === 0) { return node ? node.level || 0 : 0; } return Math.max(...node.children.map((child) => this.calculateMaxDepth(child))); } hasMoreContent(node, requestedDepth) { if (!node) return false; // Check if any children were not fully explored if (node.children) { return node.children.some((child) => child.children === null || this.hasMoreContent(child, requestedDepth)); } return false; } countArtifactsRecursively(node) { let count = node.artifacts?.length || 0; if (node.children) { count += node.children.reduce((sum, child) => sum + this.countArtifactsRecursively(child), 0); } return count; } collectMetadataRecursively(node, stats) { // Collect from direct artifacts if (node.artifacts) { node.artifacts.forEach((artifact) => { if (artifact.technology) stats.technologies.add(artifact.technology); if (artifact.metadata?.category) stats.categories.add(artifact.metadata.category); if (artifact.metadata?.author) stats.authors.add(artifact.metadata.author); }); } // Collect from children if (node.children) { node.children.forEach((child) => this.collectMetadataRecursively(child, stats)); } } getLastActivityRecursively(node) { const activities = []; // Get activity from direct artifacts const nodeActivity = this.getLastActivity(node.artifacts || []); if (nodeActivity) activities.push(nodeActivity); // Get activity from children if (node.children) { node.children.forEach((child) => { const childActivity = this.getLastActivityRecursively(child); if (childActivity) activities.push(childActivity); }); } if (activities.length === 0) return null; return activities.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())[0]; } calculatePopularityScore(node) { // Simple popularity calculation based on artifact count and recency const artifactCount = this.countArtifactsRecursively(node); const lastActivity = this.getLastActivityRecursively(node); let score = artifactCount * 10; // Base score from content if (lastActivity) { const daysSinceActivity = (Date.now() - new Date(lastActivity).getTime()) / (1000 * 60 * 60 * 24); const recencyScore = Math.max(0, 100 - daysSinceActivity); score += recencyScore; } return Math.round(score); } findInterestingPaths(node, interestingPaths) { if (!node) return; const artifactCount = this.countArtifactsRecursively(node); // Consider paths with significant content as interesting if (artifactCount >= 5 && node.path) { interestingPaths.push(`${node.path} (${artifactCount} artifacts)`); } // Recursively check children if (node.children) { node.children.forEach((child) => this.findInterestingPaths(child, interestingPaths)); } } } //# sourceMappingURL=ExploreNamespaceController.js.map