UNPKG

@dollhousemcp/mcp-server

Version:

DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.

640 lines (636 loc) 99 kB
import { MCPInputValidator, sanitizeInput } from '../security/InputValidator.js'; import { SecureErrorHandler } from '../security/errorHandler.js'; import { ElementType } from '../portfolio/PortfolioManager.js'; import { logger } from '../utils/logger.js'; import { normalizeElementType } from '../utils/elementTypeNormalization.js'; import * as path from 'path'; import { FileDiscoveryUtil } from '../utils/FileDiscoveryUtil.js'; import { getSourceIcon } from '../utils/index.js'; import { SecurityMonitor } from '../security/securityMonitor.js'; /** * CollectionHandler - Manages DollhouseMCP collection browsing, search, and content installation * * Uses dependency injection for all services: * - InitializationService for setup tasks * - PersonaIndicatorService for persona indicator formatting * - PersonaManager for persona management * * FIX: DMCP-SEC-006 - Security audit suppression * This handler delegates collection operations to specialized services. * Audit logging happens in the underlying services (CollectionBrowser, ElementInstaller, etc.). * @security-audit-suppress DMCP-SEC-006 */ export class CollectionHandler { collectionBrowser; collectionSearch; personaDetails; elementInstaller; collectionCache; portfolioManager; apiCache; personaManager; submitToPortfolioTool; unifiedIndexManager; initService; indicatorService; fileOperations; constructor(collectionBrowser, collectionSearch, personaDetails, elementInstaller, collectionCache, portfolioManager, apiCache, personaManager, submitToPortfolioTool, unifiedIndexManager, initService, indicatorService, fileOperations) { this.collectionBrowser = collectionBrowser; this.collectionSearch = collectionSearch; this.personaDetails = personaDetails; this.elementInstaller = elementInstaller; this.collectionCache = collectionCache; this.portfolioManager = portfolioManager; this.apiCache = apiCache; this.personaManager = personaManager; this.submitToPortfolioTool = submitToPortfolioTool; this.unifiedIndexManager = unifiedIndexManager; this.initService = initService; this.indicatorService = indicatorService; this.fileOperations = fileOperations; } async browseCollection(section, type) { try { // FIX #471: Replace legacy category validation with proper section/type validation // Valid sections: library, showcase, catalog // Valid types for MCP: personas, skills, agents, templates (others filtered per Issue #144) // Note: tools, prompts, ensembles, memories exist in collection but are filtered from MCP const validSections = ['library', 'showcase', 'catalog']; // ⚠️ CRITICAL: When adding new element types, you MUST update this array! // See docs/developer-guide/ADDING_NEW_ELEMENT_TYPES_CHECKLIST.md for complete checklist // This array is often forgotten and causes validation failures for new types const validTypes = ['personas', 'skills', 'agents', 'templates', 'memories']; // Only MCP-supported types // Validate section if provided const validatedSection = section ? sanitizeInput(section.toLowerCase(), 100) : undefined; if (validatedSection && !validSections.includes(validatedSection)) { throw new Error(`Invalid section '${validatedSection}'. Must be one of: ${validSections.join(', ')}`); } // Validate type if provided (only valid when section is 'library') // Issue #433: Accept singular forms (e.g., "memory" → "memories") let validatedType = type ? sanitizeInput(type.toLowerCase(), 100) : undefined; if (validatedType && validatedSection === 'library') { const normalizedType = normalizeElementType(validatedType); if (!normalizedType || !validTypes.includes(normalizedType)) { throw new Error(`Invalid type '${validatedType}'. Must be one of: ${validTypes.join(', ')}`); } validatedType = normalizedType; } if (validatedType && validatedSection !== 'library') { throw new Error('Type parameter is only valid when section is "library"'); } const result = await this.collectionBrowser.browseCollection(validatedSection, validatedType); // Handle sections view const items = result.items; const categories = result.sections || result.categories; const text = this.collectionBrowser.formatBrowseResults(items, categories, validatedSection, validatedType, this.indicatorService.getPersonaIndicator()); return { content: [ { type: "text", text: text, }, ], }; } catch (error) { const sanitized = SecureErrorHandler.sanitizeError(error); return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}❌ Collection browsing failed: ${sanitized.message}`, }, ], }; } } async searchCollection(query) { try { // Enhanced input validation for search query const validatedQuery = MCPInputValidator.validateSearchQuery(query); const items = await this.collectionSearch.searchCollection(validatedQuery); const text = this.collectionSearch.formatSearchResults(items, validatedQuery, this.indicatorService.getPersonaIndicator()); return { content: [ { type: "text", text: text, }, ], }; } catch (error) { const sanitized = SecureErrorHandler.sanitizeError(error); return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}❌ Error searching collection: ${sanitized.message}`, }, ], }; } } async searchCollectionEnhanced(query, options = {}) { try { // Enhanced input validation for search query const validatedQuery = MCPInputValidator.validateSearchQuery(query); // Validate and sanitize options const validatedOptions = { elementType: options.elementType ? String(options.elementType) : undefined, category: options.category ? String(options.category) : undefined, page: options.page ? Math.max(1, Number.parseInt(options.page) || 1) : 1, pageSize: options.pageSize ? Math.min(100, Math.max(1, Number.parseInt(options.pageSize) || 25)) : 25, sortBy: options.sortBy && ['relevance', 'name', 'date'].includes(options.sortBy) ? options.sortBy : 'relevance' }; const results = await this.collectionSearch.searchCollectionWithOptions(validatedQuery, validatedOptions); const text = this.collectionSearch.formatSearchResultsWithPagination(results, this.indicatorService.getPersonaIndicator()); return { content: [ { type: "text", text: text, }, ], }; } catch (error) { const sanitized = SecureErrorHandler.sanitizeError(error); return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}❌ Error searching collection: ${sanitized.message}`, }, ], }; } } async getCollectionContent(path) { try { const { metadata, content } = await this.personaDetails.getCollectionContent(path); const text = this.personaDetails.formatPersonaDetails(metadata, content, path, this.indicatorService.getPersonaIndicator()); return { content: [ { type: "text", text: text, }, ], }; } catch (error) { const sanitized = SecureErrorHandler.sanitizeError(error); return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}❌ Error fetching content: ${sanitized.message}`, }, ], }; } } async installContent(inputPath) { try { const result = await this.elementInstaller.installContent(inputPath); if (!result.success) { return { content: [ { type: "text", text: `⚠️ ${result.message}`, }, ], }; } // If it's a persona, reload personas if (result.elementType === ElementType.PERSONA) { await this.personaManager.reload(); } // FIX: DMCP-SEC-006 - Add security audit logging for content installation SecurityMonitor.logSecurityEvent({ type: 'ELEMENT_CREATED', severity: 'LOW', source: 'CollectionHandler.installContent', details: `Element installed: ${result.elementType}/${result.metadata?.name}`, additionalData: { elementType: result.elementType, filename: result.filename } }); const text = this.elementInstaller.formatInstallSuccess(result.metadata, result.filename, result.elementType); return { content: [ { type: "text", text: text, }, ], }; } catch (error) { const sanitized = SecureErrorHandler.sanitizeError(error); return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}❌ Error installing AI customization element: ${sanitized.message}`, }, ], }; } } async submitContent(contentIdentifier) { try { // Try to find the content across all element types let elementType; let foundPath = null; // PERFORMANCE OPTIMIZATION: Search all element directories in parallel // NOTE: This dynamically handles ALL element types from the ElementType enum // No hardcoded count - if you add 10 more element types tomorrow, this code // will automatically search all 16 types without any changes needed here const searchPromises = Object.values(ElementType).map(async (type) => { const dir = this.portfolioManager.getElementDir(type); try { const file = await FileDiscoveryUtil.findFile(dir, contentIdentifier, { extensions: ['.md', '.json', '.yaml', '.yml'], partialMatch: true, cacheResults: true }); return file ? { type: type, file } : null; } catch (error) { // IMPROVED ERROR HANDLING: Log warnings for unexpected errors if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') { // Not just a missing directory - this could be a permission issue or other problem logger.warn(`Unexpected error searching ${type} directory`, { contentIdentifier, type, error: error?.message || String(error), code: error?.code }); } else { // Directory doesn't exist - this is expected for unused element types logger.debug(`${type} directory does not exist, skipping`, { type }); } return null; } }); // Wait for all searches to complete and find the first match const searchResults = await Promise.allSettled(searchPromises); // NOTE: File validation - we rely on the portfolio directory structure to ensure // files are in the correct element type directory. Additional schema validation // could be added here if needed, but the current approach is sufficient as: // 1. FileDiscoveryUtil already validates file extensions // 2. Portfolio structure enforces proper organization // 3. submitToPortfolioTool performs additional validation downstream for (const result of searchResults) { if (result.status === 'fulfilled' && result.value) { foundPath = result.value.file; elementType = result.value.type; logger.debug(`Found content in ${elementType} directory`, { contentIdentifier, type: elementType, file: foundPath }); break; } } // CRITICAL FIX: Never default to any element type when content is not found // This prevents incorrect submissions and forces proper type detection or user specification if (!elementType) { // Content not found in any element directory - provide helpful error with suggestions const availableTypes = Object.values(ElementType).join(', '); logger.warn(`Content "${contentIdentifier}" not found in any portfolio directory`, { contentIdentifier, searchedTypes: Object.values(ElementType) }); // UX IMPROVEMENT: Enhanced error message with smart suggestions let errorMessage = `❌ Content "${contentIdentifier}" not found in portfolio.\n\n`; errorMessage += `🔍 **Searched across all element types**: ${availableTypes}\n\n`; // Try to provide smart suggestions based on partial matches try { const suggestions = []; // Search for similar names across all element types for (const elementType of Object.values(ElementType)) { const dir = this.portfolioManager.getElementDir(elementType); try { const partialMatches = await FileDiscoveryUtil.findFile(dir, contentIdentifier, { extensions: ['.md', '.json', '.yaml', '.yml'], partialMatch: true, cacheResults: false }); if (Array.isArray(partialMatches) && partialMatches.length > 0) { for (const match of partialMatches.slice(0, 2)) { const basename = path.basename(match, path.extname(match)); suggestions.push(`"${basename}" (${elementType})`); } } else if (partialMatches) { const basename = path.basename(partialMatches, path.extname(partialMatches)); suggestions.push(`"${basename}" (${elementType})`); } } catch { // Skip this type if there's an error continue; } } if (suggestions.length > 0) { errorMessage += `💡 **Did you mean one of these?**\n`; for (const suggestion of suggestions.slice(0, 5)) { errorMessage += ` • ${suggestion}\n`; } errorMessage += `\n`; } } catch (suggestionError) { // If suggestions fail, continue without them logger.debug('Failed to generate suggestions', { suggestionError }); } errorMessage += `🛠️ **Step-by-step troubleshooting**:\n`; errorMessage += `1. 📝 **List all content**: Use \`list_portfolio\` to see what's available\n`; errorMessage += `2. 🔍 **Check spelling**: Verify the exact name and try variations `; errorMessage += `3. 🎯 **Specify type**: Try \`submit_collection_content \"${contentIdentifier}\" --type=personas\`\n\n`; errorMessage += `4. 📁 **Browse files**: Check your portfolio directory manually `; errorMessage += `📝 **Tip**: The system searches both filenames and display names with fuzzy matching.`; return { content: [ { type: "text", text: errorMessage, }, ], }; } // Check for duplicates across all sources before submission try { // Extract the actual element name from the content path const basename = path.basename(foundPath, path.extname(foundPath)); const duplicates = await this.unifiedIndexManager.checkDuplicates(basename); if (duplicates.length > 0) { const duplicate = duplicates[0]; let warningText = `⚠️ **Duplicate Detection Alert**\n\n`; warningText += `Found "${duplicate.name}" in multiple sources:\n\n`; for (const source of duplicate.sources) { const sourceIcon = getSourceIcon(source.source); warningText += `${sourceIcon} **${source.source}**: ${source.version || 'unknown version'} (${source.lastModified.toLocaleDateString()})\n`; } warningText += `\n`; if (duplicate.hasVersionConflict && duplicate.versionConflict) { warningText += `🔄 **Version Conflict Detected**\n`; warningText += `Recommended source: **${duplicate.versionConflict.recommended}**\n`; warningText += `Reason: ${duplicate.versionConflict.reason}\n\n`; } warningText += `**Recommendations:**\n`; warningText += `• Review existing versions before submitting `; warningText += `• Consider updating local version instead of creating duplicate `; warningText += `• Ensure your version adds meaningful improvements `; warningText += `• Update version number in metadata if submitting enhancement `; warningText += `**Proceeding with submission anyway...**\n\n`; // Log the duplicate detection for monitoring logger.warn('Duplicate content detected during submission', { contentIdentifier, elementType, duplicateInfo: duplicate }); // Continue with submission but show warning const result = await this.submitToPortfolioTool.execute({ name: contentIdentifier, type: elementType }); // Combine warning with submission result const responseText = `${this.indicatorService.getPersonaIndicator()}${result.success ? '⚠️' : '❌'} ${warningText}${result.message}`; return { content: [{ type: "text", text: responseText, }], }; } } catch (duplicateError) { // If duplicate checking fails, log but continue with submission logger.warn('Duplicate checking failed during submission', { contentIdentifier, error: duplicateError instanceof Error ? duplicateError.message : String(duplicateError) }); } // Execute the submission with the detected element type const result = await this.submitToPortfolioTool.execute({ name: contentIdentifier, type: elementType }); // Format the response - the message already contains all details let responseText = result.message; // Add persona indicator for consistency responseText = `${this.indicatorService.getPersonaIndicator()}${result.success ? '✅' : '❌'} ${responseText}`; return { content: [ { type: "text", text: responseText, }, ], }; } catch (error) { // UX IMPROVEMENT: Comprehensive error handling with fallback suggestions logger.error('Unexpected error in submitContent', { contentIdentifier, error: error.message, stack: error.stack }); let errorMessage = `${this.indicatorService.getPersonaIndicator()}❌ **Submission Failed**\n\n`; errorMessage += `🚨 **Error**: ${error.message || 'Unknown error occurred'}\n\n`; // Provide contextual troubleshooting based on error type if (error.message?.includes('auth') || error.message?.includes('token')) { errorMessage += `🔐 **Authentication Issue**:\n`; errorMessage += `• Run: \`setup_github_auth\` to re-authenticate\n`; errorMessage += `• Check: \`gh auth status\` if you have GitHub CLI\n\n`; } if (error.message?.includes('network') || error.message?.includes('connection')) { errorMessage += `🌐 **Network Issue**:\n`; errorMessage += `• Check your internet connection `; errorMessage += `• Try again in a few minutes `; errorMessage += `• Check GitHub status: https://status.github.com `; } errorMessage += `🚑 **Emergency Alternatives**:\n`; errorMessage += `1. 🔄 **Retry**: Try the same command again `; errorMessage += `2. 📝 **Check content**: Use \`list_portfolio\` to verify the element exists\n`; errorMessage += `3. 🎯 **Specify type**: Add \`--type=personas\` if you know the element type\n`; errorMessage += `4. 🚑 **Manual upload**: Copy content directly to GitHub via web interface `; errorMessage += `📞 **Need help?** This looks like a system issue. Please report it with the error details above.`; return { content: [ { type: "text", text: errorMessage, }, ], }; } } /** * Configure collection submission settings * Controls whether content is automatically submitted to the DollhouseMCP collection */ async configureCollectionSubmission(autoSubmit) { try { // Store the configuration in environment variable if (autoSubmit) { process.env.DOLLHOUSE_AUTO_SUBMIT_TO_COLLECTION = 'true'; } else { delete process.env.DOLLHOUSE_AUTO_SUBMIT_TO_COLLECTION; } const message = autoSubmit ? "✅ Collection submission enabled! Content will automatically be submitted to the DollhouseMCP collection after portfolio upload." : "✅ Collection submission disabled. Content will only be uploaded to your personal portfolio."; return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}${message}` } ] }; } catch (error) { logger.error('Error configuring collection submission', { error }); return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}❌ Failed to configure collection submission: ${error instanceof Error ? error.message : 'Unknown error'}` } ] }; } } /** * Get current collection submission configuration * Shows whether auto-submit is enabled or disabled */ async getCollectionSubmissionConfig() { const autoSubmitEnabled = process.env.DOLLHOUSE_AUTO_SUBMIT_TO_COLLECTION === 'true'; const message = `**Collection Submission Configuration**\n\n` + `• **Auto-submit**: ${autoSubmitEnabled ? '✅ Enabled' : '❌ Disabled'}\n\n` + `When auto-submit is enabled, the \`submit_collection_content\` tool will:\n` + `1. Upload content to your GitHub portfolio\n` + `2. Automatically create a submission issue in DollhouseMCP/collection\n\n` + `To change this setting, use:\n` + `\`\`\`\nconfigure_collection_submission autoSubmit: true/false\n\`\`\``; return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}${message}` } ] }; } async getCollectionCacheHealth() { try { // Get cache statistics from both caches const collectionStats = await this.collectionCache.getCacheStats(); const searchStats = await this.collectionSearch.getCacheStats(); // Check if cache directory exists const cacheDir = path.join(process.cwd(), '.dollhousemcp', 'cache'); let cacheFileExists = false; let cacheFileSize = 0; try { const cacheFile = path.join(cacheDir, 'collection-cache.json'); const fileStats = await this.fileOperations.stat(cacheFile); cacheFileExists = true; cacheFileSize = fileStats.size; } catch { // Cache file doesn't exist yet } // Format cache age const formatAge = (ageMs) => { if (ageMs === 0) return 'Not cached'; const hours = Math.floor(ageMs / (1000 * 60 * 60)); const minutes = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60)); if (hours > 0) { return `${hours}h ${minutes}m old`; } return `${minutes}m old`; }; // Build health report with both cache systems const healthReport = { collection: { status: collectionStats.isValid ? 'healthy' : (cacheFileExists ? 'expired' : 'empty'), cacheExists: cacheFileExists, itemCount: collectionStats.itemCount, cacheAge: formatAge(collectionStats.cacheAge), cacheAgeMs: collectionStats.cacheAge, isValid: collectionStats.isValid, cacheFileSize: cacheFileSize, cacheFileSizeFormatted: cacheFileSize > 0 ? `${(cacheFileSize / 1024).toFixed(2)} KB` : '0 KB', ttlRemaining: collectionStats.isValid ? formatAge(24 * 60 * 60 * 1000 - collectionStats.cacheAge) : 'Expired' }, index: { status: searchStats.index.isValid ? 'healthy' : (searchStats.index.hasCache ? 'expired' : 'empty'), hasCache: searchStats.index.hasCache, elements: searchStats.index.elements, cacheAge: formatAge(searchStats.index.age), isValid: searchStats.index.isValid, ttlRemaining: searchStats.index.isValid ? formatAge(15 * 60 * 1000 - searchStats.index.age) : 'Expired' }, overall: { recommendation: (collectionStats.isValid || searchStats.index.isValid) ? 'Cache system is operational and serving content efficiently' : 'Cache system will refresh on next access for optimal performance' } }; return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}📊 **Collection Cache Health Check**\n\n` + `## 🗄️ Collection Cache (Legacy)\n` + `**Status**: ${healthReport.collection.status === 'healthy' ? '✅' : healthReport.collection.status === 'expired' ? '⚠️' : '📦'} ${healthReport.collection.status.toUpperCase()}\n` + `**Items Cached**: ${healthReport.collection.itemCount}\n` + `**Cache Age**: ${healthReport.collection.cacheAge}\n` + `**Cache Size**: ${healthReport.collection.cacheFileSizeFormatted}\n` + `**TTL Remaining**: ${healthReport.collection.ttlRemaining}\n\n` + `## 🚀 Index Cache (Enhanced Search)\n` + `**Status**: ${healthReport.index.status === 'healthy' ? '✅' : healthReport.index.status === 'expired' ? '⚠️' : '📦'} ${healthReport.index.status.toUpperCase()}\n` + `**Elements Indexed**: ${healthReport.index.elements}\n` + `**Cache Age**: ${healthReport.index.cacheAge}\n` + `**TTL Remaining**: ${healthReport.index.ttlRemaining}\n\n` + `**Overall Status**: ${healthReport.overall.recommendation}\n\n` + `The enhanced index cache provides fast search with pagination, filtering, and sorting. ` + `The collection cache serves as a fallback for offline browsing.`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Failed to get cache health: ${errorMessage}`); return { content: [ { type: "text", text: `${this.indicatorService.getPersonaIndicator()}❌ Failed to get cache health: ${errorMessage}`, }, ], }; } } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiQ29sbGVjdGlvbkhhbmRsZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaGFuZGxlcnMvQ29sbGVjdGlvbkhhbmRsZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBR0EsT0FBTyxFQUFFLGlCQUFpQixFQUFFLGFBQWEsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQ2pGLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQ2pFLE9BQU8sRUFBRSxXQUFXLEVBQW9CLE1BQU0sa0NBQWtDLENBQUM7QUFDakYsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLHNDQUFzQyxDQUFDO0FBQzVFLE9BQU8sS0FBSyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBQzdCLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBS2xFLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUlsRCxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFFakU7Ozs7Ozs7Ozs7OztHQVlHO0FBQ0gsTUFBTSxPQUFPLGlCQUFpQjtJQUVMO0lBQ0E7SUFDQTtJQUNBO0lBQ0E7SUFDQTtJQUNBO0lBQ0E7SUFDQTtJQUNBO0lBQ0E7SUFDQTtJQUNBO0lBYnJCLFlBQ3FCLGlCQUFvQyxFQUNwQyxnQkFBa0MsRUFDbEMsY0FBOEIsRUFDOUIsZ0JBQWtDLEVBQ2xDLGVBQWdDLEVBQ2hDLGdCQUFrQyxFQUNsQyxRQUFrQixFQUNsQixjQUE4QixFQUM5QixxQkFBNEMsRUFDNUMsbUJBQXdDLEVBQ3hDLFdBQWtDLEVBQ2xDLGdCQUF5QyxFQUN6QyxjQUFxQztRQVpyQyxzQkFBaUIsR0FBakIsaUJBQWlCLENBQW1CO1FBQ3BDLHFCQUFnQixHQUFoQixnQkFBZ0IsQ0FBa0I7UUFDbEMsbUJBQWMsR0FBZCxjQUFjLENB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