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