Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
FileDiscoveryUtil.tsโ€ข6.04 kB
/** * Utility class for efficient file discovery operations * Provides optimized file search with caching and security * * IMPLEMENTATION (PR #503 - PR #496 Recommendation): * 1. PERFORMANCE: Single readdir operation instead of multiple file checks * 2. PERFORMANCE: 5-second cache TTL for repeated searches * 3. SECURITY: Unicode normalization for all search inputs * 4. MEMORY: Efficient cache management with 100 entry limit * * This addresses the PR #496 review recommendation to extract file discovery * logic to a reusable utility class for better performance and maintainability. */ import * as fs from 'fs/promises'; import * as path from 'path'; import { logger } from './logger.js'; import { UnicodeValidator } from '../security/validators/unicodeValidator.js'; import { SecurityMonitor } from '../security/securityMonitor.js'; export interface FileSearchOptions { extensions?: string[]; partialMatch?: boolean; maxResults?: number; cacheResults?: boolean; } interface CacheEntry { files: string[]; timestamp: number; } export class FileDiscoveryUtil { private static readonly CACHE_TTL = 5000; // 5 seconds cache TTL private static cache = new Map<string, CacheEntry>(); /** * Find files in a directory with optimized search * Uses single readdir operation and filters results */ static async findFiles( directory: string, searchName: string, options: FileSearchOptions = {} ): Promise<string[]> { const { extensions = ['.json', '.yaml', '.yml', '.md'], partialMatch = true, maxResults = 10, cacheResults = true } = options; // Normalize search name for security const normalizedSearch = UnicodeValidator.normalize(searchName); if (!normalizedSearch.isValid) { logger.warn('Invalid Unicode in search name', { issues: normalizedSearch.detectedIssues }); return []; } const safeName = normalizedSearch.normalizedContent.toLowerCase(); // Check cache if enabled const cacheKey = `${directory}:${safeName}:${JSON.stringify(options)}`; if (cacheResults) { const cached = this.getCached(cacheKey); if (cached) { logger.debug('File search cache hit', { directory, searchName }); return cached; } } try { // Single readdir operation for efficiency const files = await fs.readdir(directory); // Build search patterns const searchPatterns = this.buildSearchPatterns(safeName, extensions); // Filter files efficiently const matches: string[] = []; for (const file of files) { const fileLower = file.toLowerCase(); // Check each pattern for (const pattern of searchPatterns) { if (this.matchesPattern(fileLower, pattern, partialMatch)) { const fullPath = path.join(directory, file); matches.push(fullPath); if (matches.length >= maxResults) { break; } } } if (matches.length >= maxResults) { break; } } // Cache results if enabled if (cacheResults && matches.length > 0) { this.setCached(cacheKey, matches); } // Log file discovery for monitoring if (matches.length > 0) { logger.debug('Files discovered', { directory, searchName, count: matches.length }); } return matches; } catch (error) { logger.error('File discovery failed', { directory, searchName, error: error instanceof Error ? error.message : String(error) }); return []; } } /** * Build search patterns for different file extensions */ private static buildSearchPatterns(baseName: string, extensions: string[]): string[] { const patterns: string[] = [baseName]; // Add extension variations for (const ext of extensions) { patterns.push(`${baseName}${ext}`); } // Add common variations const nameWithoutExtension = baseName.replace(/\.[^.]+$/, ''); if (nameWithoutExtension !== baseName) { patterns.push(nameWithoutExtension); for (const ext of extensions) { patterns.push(`${nameWithoutExtension}${ext}`); } } return patterns; } /** * Check if filename matches pattern */ private static matchesPattern(filename: string, pattern: string, partialMatch: boolean): boolean { if (partialMatch) { return filename.includes(pattern); } return filename === pattern; } /** * Get cached results if still valid */ private static getCached(key: string): string[] | null { const entry = this.cache.get(key); if (!entry) return null; const age = Date.now() - entry.timestamp; if (age > this.CACHE_TTL) { this.cache.delete(key); return null; } return entry.files; } /** * Cache results with timestamp */ private static setCached(key: string, files: string[]): void { // Limit cache size to prevent memory issues if (this.cache.size > 100) { // Remove oldest entries const entries = Array.from(this.cache.entries()); entries.sort((a, b) => a[1].timestamp - b[1].timestamp); for (let i = 0; i < 50; i++) { this.cache.delete(entries[i][0]); } } this.cache.set(key, { files, timestamp: Date.now() }); } /** * Clear the cache */ static clearCache(): void { this.cache.clear(); logger.debug('File discovery cache cleared'); } /** * Find a single file (convenience method) */ static async findFile( directory: string, searchName: string, options?: FileSearchOptions ): Promise<string | null> { const files = await this.findFiles(directory, searchName, { ...options, maxResults: 1 }); return files.length > 0 ? files[0] : null; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DollhouseMCP/DollhouseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server