UNPKG

fast-filesystem-mcp

Version:

Fast Filesystem MCP Server - Advanced file operations with Auto-Chunking, Sequential Reading, complex file operations (copy, move, delete, batch, compress), optimized for Claude Desktop

966 lines 39 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { promises as fs } from 'fs'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); // Claude 최적화 설정 const CLAUDE_MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB const CLAUDE_MAX_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB const CLAUDE_MAX_LINES = 2000; // 최대 2000줄 const CLAUDE_MAX_DIR_ITEMS = 1000; // 디렉토리 항목 최대 1000개 // 기본 허용 디렉토리들 const DEFAULT_ALLOWED_DIRECTORIES = [ process.env.HOME || '/home', '/tmp', '/Users', '/home' ]; // 기본 제외 패턴 (보안 및 성능) const DEFAULT_EXCLUDE_PATTERNS = [ '.venv', 'venv', 'node_modules', '.git', '.svn', '.hg', '__pycache__', '.pytest_cache', '.mypy_cache', '.coverage', 'dist', 'build', 'target', 'bin', 'obj', '.vs', '.vscode', '*.pyc', '*.pyo', '*.pyd', '.DS_Store', 'Thumbs.db' ]; // 유틸리티 함수들 function isPathAllowed(targetPath) { const absolutePath = path.resolve(targetPath); return DEFAULT_ALLOWED_DIRECTORIES.some(allowedDir => absolutePath.startsWith(path.resolve(allowedDir))); } function safePath(inputPath) { if (!isPathAllowed(inputPath)) { throw new Error(`Access denied to path: ${inputPath}`); } return path.resolve(inputPath); } function formatSize(bytes) { const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)} ${units[unitIndex]}`; } function shouldExcludePath(targetPath, excludePatterns = []) { const patterns = [...DEFAULT_EXCLUDE_PATTERNS, ...excludePatterns]; const pathName = path.basename(targetPath).toLowerCase(); const pathParts = targetPath.split(path.sep); return patterns.some(pattern => { const patternLower = pattern.toLowerCase(); if (pattern.includes('*') || pattern.includes('?')) { const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.')); return regex.test(pathName); } return pathParts.some(part => part.toLowerCase() === patternLower) || pathName === patternLower; }); } function truncateContent(content, maxSize = CLAUDE_MAX_RESPONSE_SIZE) { const contentBytes = Buffer.byteLength(content, 'utf8'); if (contentBytes <= maxSize) { return { content, truncated: false }; } let truncated = content; while (Buffer.byteLength(truncated, 'utf8') > maxSize) { truncated = truncated.slice(0, -1); } return { content: truncated, truncated: true, original_size: contentBytes, truncated_size: Buffer.byteLength(truncated, 'utf8') }; } // 대용량 파일 작성을 위한 유틸리티 함수들 async function writeFileWithRetry(filePath, content, encoding, chunkSize, maxRetries, append) { let retryCount = 0; const startTime = Date.now(); while (retryCount <= maxRetries) { try { await writeFileStreaming(filePath, content, encoding, chunkSize, append); return { retryCount, totalTime: Date.now() - startTime }; } catch (error) { retryCount++; if (retryCount > maxRetries) { throw error; } const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 10000); await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error('Max retry attempts exceeded'); } async function writeFileStreaming(filePath, content, encoding, chunkSize, append) { const buffer = Buffer.from(content, encoding); const fileHandle = await fs.open(filePath, append ? 'a' : 'w'); try { let position = 0; while (position < buffer.length) { const end = Math.min(position + chunkSize, buffer.length); const chunk = buffer.subarray(position, end); await fileHandle.write(chunk); position = end; // 메모리 압박 방지를 위한 이벤트 루프 양보 if (position % (chunkSize * 10) === 0) { await new Promise(resolve => setImmediate(resolve)); } } await fileHandle.sync(); // 디스크에 강제 동기화 } finally { await fileHandle.close(); } } async function checkDiskSpace(dirPath, requiredBytes) { try { const { stdout } = await execAsync(`df -B1 "${dirPath}" | tail -1 | awk '{print $4}'`); const availableBytes = parseInt(stdout.trim()); if (availableBytes < requiredBytes * 1.5) { throw new Error(`Insufficient disk space. Required: ${formatSize(requiredBytes)}, ` + `Available: ${formatSize(availableBytes)}`); } } catch (error) { console.warn('Could not check disk space:', error); } } async function getOriginalFileSize(filePath) { try { const stats = await fs.stat(filePath); return stats.size; } catch { return 0; } } // MCP 서버 생성 const server = new Server({ name: 'fast-filesystem', version: '2.4.1', }, { capabilities: { tools: {}, }, }); // 툴 목록 정의 server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'fast_list_allowed_directories', description: '허용된 디렉토리 목록을 조회합니다', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'fast_read_file', description: '파일을 읽습니다 (청킹 지원)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '읽을 파일 경로' }, start_offset: { type: 'number', description: '시작 바이트 위치' }, max_size: { type: 'number', description: '읽을 최대 크기' }, line_start: { type: 'number', description: '시작 라인 번호' }, line_count: { type: 'number', description: '읽을 라인 수' }, encoding: { type: 'string', description: '텍스트 인코딩', default: 'utf-8' } }, required: ['path'] } }, { name: 'fast_write_file', description: '파일을 쓰거나 수정합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '파일 경로' }, content: { type: 'string', description: '파일 내용' }, encoding: { type: 'string', description: '텍스트 인코딩', default: 'utf-8' }, create_dirs: { type: 'boolean', description: '디렉토리 자동 생성', default: true }, append: { type: 'boolean', description: '추가 모드', default: false } }, required: ['path', 'content'] } }, { name: 'fast_large_write_file', description: '대용량 파일을 안정적으로 작성합니다 (스트리밍, 재시도, 백업, 검증 기능)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '파일 경로' }, content: { type: 'string', description: '파일 내용' }, encoding: { type: 'string', description: '텍스트 인코딩', default: 'utf-8' }, create_dirs: { type: 'boolean', description: '디렉토리 자동 생성', default: true }, append: { type: 'boolean', description: '추가 모드', default: false }, chunk_size: { type: 'number', description: '청크 크기 (바이트)', default: 65536 }, backup: { type: 'boolean', description: '기존 파일 백업 생성', default: true }, retry_attempts: { type: 'number', description: '재시도 횟수', default: 3 }, verify_write: { type: 'boolean', description: '작성 후 검증', default: true } }, required: ['path', 'content'] } }, { name: 'fast_list_directory', description: '디렉토리 목록을 조회합니다 (페이징 지원)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '디렉토리 경로' }, page: { type: 'number', description: '페이지 번호', default: 1 }, page_size: { type: 'number', description: '페이지당 항목 수' }, pattern: { type: 'string', description: '파일명 필터 패턴' }, show_hidden: { type: 'boolean', description: '숨김 파일 표시', default: false }, sort_by: { type: 'string', description: '정렬 기준', enum: ['name', 'size', 'modified', 'type'], default: 'name' }, reverse: { type: 'boolean', description: '역순 정렬', default: false } }, required: ['path'] } }, { name: 'fast_get_file_info', description: '파일/디렉토리 상세 정보를 조회합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '조회할 경로' } }, required: ['path'] } }, { name: 'fast_create_directory', description: '디렉토리를 생성합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '생성할 디렉토리 경로' }, recursive: { type: 'boolean', description: '재귀적 생성', default: true } }, required: ['path'] } }, { name: 'fast_search_files', description: '파일을 검색합니다 (이름/내용)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '검색할 디렉토리' }, pattern: { type: 'string', description: '검색 패턴' }, content_search: { type: 'boolean', description: '파일 내용 검색', default: false }, case_sensitive: { type: 'boolean', description: '대소문자 구분', default: false }, max_results: { type: 'number', description: '최대 결과 수', default: 100 } }, required: ['path', 'pattern'] } }, { name: 'fast_get_directory_tree', description: '디렉토리 트리 구조를 가져옵니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '루트 디렉토리 경로' }, max_depth: { type: 'number', description: '최대 깊이', default: 3 }, show_hidden: { type: 'boolean', description: '숨김 파일 표시', default: false }, include_files: { type: 'boolean', description: '파일 포함', default: true } }, required: ['path'] } }, { name: 'fast_get_disk_usage', description: '디스크 사용량을 조회합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '조회할 경로', default: '/' } } } }, { name: 'fast_find_large_files', description: '큰 파일들을 찾습니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '검색할 디렉토리' }, min_size: { type: 'string', description: '최소 크기 (예: 100MB, 1GB)', default: '100MB' }, max_results: { type: 'number', description: '최대 결과 수', default: 50 } }, required: ['path'] } } ], }; }); // 툴 호출 핸들러 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let result; switch (name) { case 'fast_list_allowed_directories': result = await handleListAllowedDirectories(); break; case 'fast_read_file': result = await handleReadFile(args); break; case 'fast_write_file': result = await handleWriteFile(args); break; case 'fast_large_write_file': result = await handleLargeWriteFile(args); break; case 'fast_list_directory': result = await handleListDirectory(args); break; case 'fast_get_file_info': result = await handleGetFileInfo(args); break; case 'fast_create_directory': result = await handleCreateDirectory(args); break; case 'fast_search_files': result = await handleSearchFiles(args); break; case 'fast_get_directory_tree': result = await handleGetDirectoryTree(args); break; case 'fast_get_disk_usage': result = await handleGetDiskUsage(args); break; case 'fast_find_large_files': result = await handleFindLargeFiles(args); break; default: throw new Error(`Tool not implemented: ${name}`); } return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } catch (error) { throw new Error(error instanceof Error ? error.message : 'Unknown error'); } }); // 툴 핸들러 함수들 async function handleListAllowedDirectories() { return { allowed_directories: DEFAULT_ALLOWED_DIRECTORIES, current_working_directory: process.cwd(), exclude_patterns: DEFAULT_EXCLUDE_PATTERNS, claude_limits: { max_response_size_mb: CLAUDE_MAX_RESPONSE_SIZE / (1024 ** 2), max_chunk_size_mb: CLAUDE_MAX_CHUNK_SIZE / (1024 ** 2), max_lines_per_read: CLAUDE_MAX_LINES, max_dir_items: CLAUDE_MAX_DIR_ITEMS }, server_info: { name: 'fast-filesystem', version: '2.4.1', timestamp: new Date().toISOString() } }; } async function handleReadFile(args) { const { path: filePath, start_offset = 0, max_size, line_start, line_count, encoding = 'utf-8' } = args; const safePath_resolved = safePath(filePath); const stats = await fs.stat(safePath_resolved); if (!stats.isFile()) { throw new Error('Path is not a file'); } const maxReadSize = max_size ? Math.min(max_size, CLAUDE_MAX_CHUNK_SIZE) : CLAUDE_MAX_CHUNK_SIZE; if (line_start !== undefined) { const linesToRead = line_count ? Math.min(line_count, CLAUDE_MAX_LINES) : CLAUDE_MAX_LINES; const fileContent = await fs.readFile(safePath_resolved, encoding); const lines = fileContent.split('\n'); const selectedLines = lines.slice(line_start, line_start + linesToRead); return { content: selectedLines.join('\n'), mode: 'lines', start_line: line_start, lines_read: selectedLines.length, total_lines: lines.length, file_size: stats.size, file_size_readable: formatSize(stats.size), encoding: encoding, path: safePath_resolved }; } const fileHandle = await fs.open(safePath_resolved, 'r'); const buffer = Buffer.alloc(maxReadSize); const { bytesRead } = await fileHandle.read(buffer, 0, maxReadSize, start_offset); await fileHandle.close(); const content = buffer.subarray(0, bytesRead).toString(encoding); const result = truncateContent(content); return { content: result.content, mode: 'bytes', start_offset: start_offset, bytes_read: bytesRead, file_size: stats.size, file_size_readable: formatSize(stats.size), encoding: encoding, truncated: result.truncated, has_more: start_offset + bytesRead < stats.size, path: safePath_resolved }; } async function handleWriteFile(args) { const { path: filePath, content, encoding = 'utf-8', create_dirs = true, append = false } = args; let targetPath; if (path.isAbsolute(filePath)) { targetPath = filePath; } else { targetPath = path.join(process.cwd(), filePath); } if (!isPathAllowed(targetPath)) { throw new Error(`Access denied to path: ${targetPath}`); } const resolvedPath = path.resolve(targetPath); if (create_dirs) { const dir = path.dirname(resolvedPath); await fs.mkdir(dir, { recursive: true }); } if (append) { await fs.appendFile(resolvedPath, content, encoding); } else { await fs.writeFile(resolvedPath, content, encoding); } const stats = await fs.stat(resolvedPath); return { message: `File ${append ? 'appended' : 'written'} successfully`, path: resolvedPath, size: stats.size, size_readable: formatSize(stats.size), encoding: encoding, mode: append ? 'append' : 'write', timestamp: new Date().toISOString() }; } // 새로운 대용량 파일 작성 핸들러 async function handleLargeWriteFile(args) { const { path: filePath, content, encoding = 'utf-8', create_dirs = true, append = false, chunk_size = 64 * 1024, // 64KB 청크 backup = true, retry_attempts = 3, verify_write = true } = args; let targetPath; if (path.isAbsolute(filePath)) { targetPath = filePath; } else { targetPath = path.join(process.cwd(), filePath); } if (!isPathAllowed(targetPath)) { throw new Error(`Access denied to path: ${targetPath}`); } const resolvedPath = path.resolve(targetPath); const tempPath = `${resolvedPath}.tmp.${Date.now()}`; const backupPath = `${resolvedPath}.backup.${Date.now()}`; try { // 1. 디렉토리 생성 if (create_dirs) { const dir = path.dirname(resolvedPath); await fs.mkdir(dir, { recursive: true }); } // 2. 디스크 공간 확인 const contentSize = Buffer.byteLength(content, encoding); await checkDiskSpace(path.dirname(resolvedPath), contentSize); // 3. 기존 파일 백업 (덮어쓰기 모드이고 파일이 존재할 경우) let originalExists = false; let originalSize = 0; if (!append && backup) { try { await fs.access(resolvedPath); originalExists = true; originalSize = await getOriginalFileSize(resolvedPath); await fs.copyFile(resolvedPath, backupPath); } catch { // 원본 파일이 없으면 무시 } } // 4. 스트리밍 방식으로 대용량 파일 작성 const result = await writeFileWithRetry(append ? resolvedPath : tempPath, content, encoding, chunk_size, retry_attempts, append); // 5. 원자적 이동 (append가 아닌 경우) if (!append) { await fs.rename(tempPath, resolvedPath); } // 6. 작성 검증 (옵션) if (verify_write) { const finalStats = await fs.stat(resolvedPath); if (!append) { // 새 파일인 경우 내용 크기와 일치해야 함 if (finalStats.size !== contentSize) { throw new Error(`File size verification failed. Expected: ${contentSize}, Actual: ${finalStats.size}`); } } else { // append 모드인 경우 최소한 내용 크기만큼은 증가해야 함 const expectedMinSize = originalSize + contentSize; if (finalStats.size < expectedMinSize) { throw new Error(`File size verification failed. Expected at least: ${expectedMinSize}, Actual: ${finalStats.size}`); } } } const finalStats = await fs.stat(resolvedPath); return { message: `Large file ${append ? 'appended' : 'written'} successfully`, path: resolvedPath, size: finalStats.size, size_readable: formatSize(finalStats.size), content_size: contentSize, content_size_readable: formatSize(contentSize), encoding: encoding, mode: append ? 'append' : 'write', chunks_written: Math.ceil(contentSize / chunk_size), chunk_size: chunk_size, retry_count: result.retryCount, backup_created: originalExists && backup ? backupPath : null, timestamp: new Date().toISOString(), performance: { total_time_ms: result.totalTime, write_speed_mbps: (contentSize / (1024 * 1024)) / (result.totalTime / 1000) } }; } catch (error) { // 에러 복구 try { // 임시 파일 정리 await fs.unlink(tempPath).catch(() => { }); // 백업에서 복구 (실패한 경우) if (!append && backup) { try { await fs.copyFile(backupPath, resolvedPath); } catch { // 복구도 실패 } } } catch { // 정리 실패는 무시 } throw error; } } async function handleListDirectory(args) { const { path: dirPath, page = 1, page_size, pattern, show_hidden = false, sort_by = 'name', reverse = false } = args; const safePath_resolved = safePath(dirPath); const stats = await fs.stat(safePath_resolved); if (!stats.isDirectory()) { throw new Error('Path is not a directory'); } const pageSize = page_size ? Math.min(page_size, CLAUDE_MAX_DIR_ITEMS) : 50; const entries = await fs.readdir(safePath_resolved, { withFileTypes: true }); let filteredEntries = entries.filter(entry => { if (!show_hidden && entry.name.startsWith('.')) return false; if (shouldExcludePath(path.join(safePath_resolved, entry.name))) return false; if (pattern) { return entry.name.toLowerCase().includes(pattern.toLowerCase()); } return true; }); // 정렬 filteredEntries.sort((a, b) => { let comparison = 0; switch (sort_by) { case 'name': comparison = a.name.localeCompare(b.name); break; case 'type': const aType = a.isDirectory() ? 'directory' : 'file'; const bType = b.isDirectory() ? 'directory' : 'file'; comparison = aType.localeCompare(bType); break; default: comparison = a.name.localeCompare(b.name); } return reverse ? -comparison : comparison; }); const startIdx = (page - 1) * pageSize; const endIdx = startIdx + pageSize; const pageEntries = filteredEntries.slice(startIdx, endIdx); const items = await Promise.all(pageEntries.map(async (entry) => { try { const fullPath = path.join(safePath_resolved, entry.name); const itemStats = await fs.stat(fullPath); return { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', size: entry.isFile() ? itemStats.size : null, size_readable: entry.isFile() ? formatSize(itemStats.size) : null, modified: itemStats.mtime.toISOString(), created: itemStats.birthtime.toISOString(), permissions: itemStats.mode, path: fullPath }; } catch { return { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', size: null, size_readable: null, modified: null, created: null, permissions: null, path: path.join(safePath_resolved, entry.name) }; } })); return { path: safePath_resolved, items: items, page: page, page_size: pageSize, total_count: filteredEntries.length, total_pages: Math.ceil(filteredEntries.length / pageSize), has_more: endIdx < filteredEntries.length, sort_by: sort_by, reverse: reverse, timestamp: new Date().toISOString() }; } async function handleGetFileInfo(args) { const { path: targetPath } = args; const safePath_resolved = safePath(targetPath); const stats = await fs.stat(safePath_resolved); const info = { path: safePath_resolved, name: path.basename(safePath_resolved), type: stats.isDirectory() ? 'directory' : 'file', size: stats.size, size_readable: formatSize(stats.size), created: stats.birthtime.toISOString(), modified: stats.mtime.toISOString(), accessed: stats.atime.toISOString(), permissions: stats.mode, is_readable: true, is_writable: true }; if (stats.isFile()) { info.extension = path.extname(safePath_resolved); info.mime_type = getMimeType(safePath_resolved); if (stats.size > CLAUDE_MAX_CHUNK_SIZE) { info.claude_guide = { message: 'File is large, consider using chunked reading', recommended_chunk_size: CLAUDE_MAX_CHUNK_SIZE, total_chunks: Math.ceil(stats.size / CLAUDE_MAX_CHUNK_SIZE) }; } } else if (stats.isDirectory()) { try { const entries = await fs.readdir(safePath_resolved); info.item_count = entries.length; if (entries.length > CLAUDE_MAX_DIR_ITEMS) { info.claude_guide = { message: 'Directory has many items, consider using pagination', recommended_page_size: CLAUDE_MAX_DIR_ITEMS, total_pages: Math.ceil(entries.length / CLAUDE_MAX_DIR_ITEMS) }; } } catch { info.item_count = 'Unable to count'; } } return info; } async function handleCreateDirectory(args) { const { path: dirPath, recursive = true } = args; const safePath_resolved = safePath(dirPath); await fs.mkdir(safePath_resolved, { recursive }); return { message: 'Directory created successfully', path: safePath_resolved, recursive: recursive, timestamp: new Date().toISOString() }; } async function handleSearchFiles(args) { const { path: searchPath, pattern, content_search = false, case_sensitive = false, max_results = 100 } = args; const safePath_resolved = safePath(searchPath); const maxResults = Math.min(max_results, 200); const results = []; const searchPattern = case_sensitive ? pattern : pattern.toLowerCase(); async function searchDirectory(dirPath) { if (results.length >= maxResults) return; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (results.length >= maxResults) break; const fullPath = path.join(dirPath, entry.name); if (shouldExcludePath(fullPath)) continue; if (entry.isFile()) { const searchName = case_sensitive ? entry.name : entry.name.toLowerCase(); let matched = false; let matchType = ''; if (searchName.includes(searchPattern)) { matched = true; matchType = 'filename'; } if (!matched && content_search) { try { const stats = await fs.stat(fullPath); if (stats.size < 10 * 1024 * 1024) { // 10MB 제한 const content = await fs.readFile(fullPath, 'utf-8'); const searchContent = case_sensitive ? content : content.toLowerCase(); if (searchContent.includes(searchPattern)) { matched = true; matchType = 'content'; } } } catch { // 바이너리 파일 등 읽기 실패 무시 } } if (matched) { const stats = await fs.stat(fullPath); results.push({ path: fullPath, name: entry.name, match_type: matchType, size: stats.size, size_readable: formatSize(stats.size), modified: stats.mtime.toISOString(), extension: path.extname(fullPath) }); } } else if (entry.isDirectory()) { await searchDirectory(fullPath); } } } catch { // 권한 없는 디렉토리 등 무시 } } await searchDirectory(safePath_resolved); return { results: results, total_found: results.length, search_pattern: pattern, search_path: safePath_resolved, content_search: content_search, case_sensitive: case_sensitive, max_results_reached: results.length >= maxResults, timestamp: new Date().toISOString() }; } async function handleGetDirectoryTree(args) { const { path: rootPath, max_depth = 3, show_hidden = false, include_files = true } = args; const safePath_resolved = safePath(rootPath); async function buildTree(currentPath, currentDepth) { if (currentDepth > max_depth) return null; try { const stats = await fs.stat(currentPath); const name = path.basename(currentPath); if (!show_hidden && name.startsWith('.')) return null; if (shouldExcludePath(currentPath)) return null; const node = { name: name, path: currentPath, type: stats.isDirectory() ? 'directory' : 'file', size: stats.size, size_readable: formatSize(stats.size), modified: stats.mtime.toISOString() }; if (stats.isDirectory()) { node.children = []; try { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const childPath = path.join(currentPath, entry.name); if (entry.isDirectory()) { const childNode = await buildTree(childPath, currentDepth + 1); if (childNode) node.children.push(childNode); } else if (include_files) { const childNode = await buildTree(childPath, currentDepth + 1); if (childNode) node.children.push(childNode); } } } catch { // 권한 없는 디렉토리 node.error = 'Access denied'; } } return node; } catch { return null; } } const tree = await buildTree(safePath_resolved, 0); return { tree: tree, root_path: safePath_resolved, max_depth: max_depth, show_hidden: show_hidden, include_files: include_files, timestamp: new Date().toISOString() }; } async function handleGetDiskUsage(args) { const { path: targetPath = '/' } = args; try { const { stdout } = await execAsync(`df -h "${targetPath}"`); const lines = stdout.split('\n').filter(line => line.trim()); if (lines.length > 1) { const data = lines[1].split(/\s+/); return { filesystem: data[0], total: data[1], used: data[2], available: data[3], use_percentage: data[4], mounted_on: data[5], path: targetPath, timestamp: new Date().toISOString() }; } } catch { // Fallback for systems without df command } return { error: 'Unable to get disk usage information', path: targetPath, timestamp: new Date().toISOString() }; } async function handleFindLargeFiles(args) { const { path: searchPath, min_size = '100MB', max_results = 50 } = args; const safePath_resolved = safePath(searchPath); const maxResults = Math.min(max_results, 100); // 크기 파싱 (예: 100MB -> bytes) const parseSize = (sizeStr) => { const match = sizeStr.match(/^(\d+(\.\d+)?)\s*(B|KB|MB|GB|TB)?$/i); if (!match) return 100 * 1024 * 1024; // 기본값 100MB const value = parseFloat(match[1]); const unit = (match[3] || 'B').toUpperCase(); const units = { 'B': 1, 'KB': 1024, 'MB': 1024 * 1024, 'GB': 1024 * 1024 * 1024, 'TB': 1024 * 1024 * 1024 * 1024 }; return value * (units[unit] || 1); }; const minSizeBytes = parseSize(min_size); const results = []; async function findLargeFilesRecursive(dirPath) { if (results.length >= maxResults) return; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (results.length >= maxResults) break; const fullPath = path.join(dirPath, entry.name); if (shouldExcludePath(fullPath)) continue; if (entry.isFile()) { try { const stats = await fs.stat(fullPath); if (stats.size >= minSizeBytes) { results.push({ path: fullPath, name: entry.name, size: stats.size, size_readable: formatSize(stats.size), modified: stats.mtime.toISOString(), extension: path.extname(fullPath) }); } } catch { // 파일 접근 실패 무시 } } else if (entry.isDirectory()) { await findLargeFilesRecursive(fullPath); } } } catch { // 권한 없는 디렉토리 무시 } } await findLargeFilesRecursive(safePath_resolved); // 크기별로 정렬 (큰 것부터) results.sort((a, b) => b.size - a.size); return { results: results, total_found: results.length, search_path: safePath_resolved, min_size: min_size, min_size_bytes: minSizeBytes, max_results_reached: results.length >= maxResults, timestamp: new Date().toISOString() }; } function getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase(); const mimeTypes = { '.txt': 'text/plain', '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.pdf': 'application/pdf', '.zip': 'application/zip', '.md': 'text/markdown' }; return mimeTypes[ext] || 'application/octet-stream'; } // 서버 시작 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Fast Filesystem MCP Server running on stdio'); } main().catch((error) => { console.error('Server failed to start:', error); process.exit(1); }); //# sourceMappingURL=index-backup.js.map