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

1,119 lines (1,118 loc) 176 kB
#!/usr/bin/env node /* * Copyright 2025 efforthye * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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, spawn } from 'child_process'; import { promisify } from 'util'; import { handleReadFileWithAutoChunking, handleListDirectoryWithAutoChunking, handleSearchFilesWithAutoChunking } from './enhanced-handlers.js'; import { isPathAllowed as coreIsPathAllowed, safePath as coreSafePath, getAllowedDirectories, addAllowedDirectories } from './utils.js'; // import { searchCode, SearchResult } from './search.js'; 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개 // --- Allowed directories are managed centrally in utils.ts. // Parse CLI flags "--allow <dir>" to extend allowed set at runtime. const argv = process.argv.slice(2); const allowArgs = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--allow' && i + 1 < argv.length) { allowArgs.push(argv[i + 1]); i++; } } if (allowArgs.length > 0) { const res = addAllowedDirectories(allowArgs); if (res.skipped.length > 0) { console.warn('Some --allow paths were skipped:', res.skipped); } } // 백업 파일 설정 (환경변수나 설정으로 제어) const CREATE_BACKUP_FILES = process.env.CREATE_BACKUP_FILES === 'true'; // 기본값: false, true로 설정시만 활성화 // 기본 제외 패턴 (보안 및 성능) 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 detectEmojis(text) { const emojiRegex = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F700}-\u{1F77F}]|[\u{1F780}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA00}-\u{1FA6F}]|[\u{1FA70}-\u{1FAFF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{1F004}]|[\u{1F0CF}]|[\u{1F170}-\u{1F251}]/gu; const matches = Array.from(text.matchAll(emojiRegex)); const positions = matches.map(match => match.index || 0); return { hasEmojis: matches.length > 0, count: matches.length, positions: positions }; } // 이모지 제거 함수 (선택적) function removeEmojis(text) { const emojiRegex = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F700}-\u{1F77F}]|[\u{1F780}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA00}-\u{1FA6F}]|[\u{1FA70}-\u{1FAFF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{1F004}]|[\u{1F0CF}]|[\u{1F170}-\u{1F251}]/gu; return text.replace(emojiRegex, ''); } // 파일 타입별 이모지 가이드라인 function getEmojiGuideline(filePath) { const ext = path.extname(filePath).toLowerCase(); const fileName = path.basename(filePath).toLowerCase(); // 코드 파일들 const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h', '.cs', '.php', '.rb', '.go', '.rs', '.swift', '.kt']; // 설정 파일들 const configExtensions = ['.json', '.yml', '.yaml', '.toml', '.ini', '.cfg', '.conf']; const configFiles = ['package.json', 'tsconfig.json', 'webpack.config.js', 'dockerfile', 'makefile']; // 문서 파일들 const docExtensions = ['.md', '.txt', '.rst', '.adoc']; if (codeExtensions.includes(ext)) { return { shouldAvoidEmojis: true, reason: 'Emojis not recommended in code files', fileType: 'code' }; } if (configExtensions.includes(ext) || configFiles.includes(fileName)) { return { shouldAvoidEmojis: true, reason: 'Emojis not recommended in config files', fileType: 'config' }; } if (docExtensions.includes(ext)) { return { shouldAvoidEmojis: true, reason: 'Emojis not recommended in files', fileType: 'documentation' }; } return { shouldAvoidEmojis: true, reason: 'Emojis not recommended in files', fileType: 'general' }; } // 유틸리티 함수들 function isPathAllowed(targetPath) { return coreIsPathAllowed(targetPath); } function safePath(inputPath) { return coreSafePath(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: '3.2.4', }, { 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' }, continuation_token: { type: 'string', description: '이전 호출의 연속 토큰' }, auto_chunk: { type: 'boolean', description: '자동 청킹 활성화', default: true } }, required: ['path'] } }, { name: 'fast_read_multiple_files', description: '여러 파일의 내용을 동시에 읽습니다 (순차적 읽기 지원)', inputSchema: { type: 'object', properties: { paths: { type: 'array', items: { type: 'string' }, description: '읽을 파일 경로들' }, continuation_tokens: { type: 'object', description: '파일별 continuation token (이전 호출에서 반환된 값)' }, auto_continue: { type: 'boolean', description: '자동으로 전체 파일 읽기 (기본값: true)', default: true }, chunk_size: { type: 'number', description: '청크 크기 (바이트, 기본값: 1MB)', default: 1048576 } }, required: ['paths'] } }, { 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 }, force_remove_emojis: { type: 'boolean', description: '이모지 강제 제거 (기본값: false)', 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 }, force_remove_emojis: { type: 'boolean', description: '이모지 강제 제거 (기본값: false)', default: false } }, 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 }, continuation_token: { type: 'string', description: '이전 호출의 연속 토큰' }, auto_chunk: { type: 'boolean', description: '자동 청킹 활성화', default: true } }, 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 }, context_lines: { type: 'number', description: '매치된 라인 주변 컨텍스트 라인 수', default: 0 }, file_pattern: { type: 'string', description: '파일명 필터 패턴 (*.js, *.txt 등)', default: '' }, include_binary: { type: 'boolean', description: '바이너리 파일 포함 여부', default: false }, continuation_token: { type: 'string', description: '이전 호출의 연속 토큰' }, auto_chunk: { type: 'boolean', description: '자동 청킹 활성화', default: true } }, required: ['path', 'pattern'] } }, { name: 'fast_search_code', description: '코드 검색 (ripgrep 스타일) - 자동 청킹, 라인번호와 컨텍스트 제공', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '검색할 디렉토리' }, pattern: { type: 'string', description: '검색 패턴 (정규표현식 지원)' }, file_pattern: { type: 'string', description: '파일 확장자 필터 (*.js, *.ts 등)', default: '' }, context_lines: { type: 'number', description: '매치 주변 컨텍스트 라인 수', default: 2 }, max_results: { type: 'number', description: '최대 결과 수', default: 50 }, case_sensitive: { type: 'boolean', description: '대소문자 구분', default: false }, include_hidden: { type: 'boolean', description: '숨김 파일 포함', default: false }, max_file_size: { type: 'number', description: '검색할 최대 파일 크기 (MB)', default: 10 }, continuation_token: { type: 'string', description: '이전 호출의 연속 토큰' }, auto_chunk: { type: 'boolean', description: '자동 청킹 활성화', default: true } }, 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'] } }, { name: 'fast_edit_block', description: '정교한 블록 편집: 정확한 문자열 매칭으로 안전한 편집 (desktop-commander 방식)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '편집할 파일 경로' }, old_text: { type: 'string', description: '정확히 매칭할 기존 텍스트 (최소 컨텍스트 포함)' }, new_text: { type: 'string', description: '새로운 텍스트' }, expected_replacements: { type: 'number', description: '예상 교체 횟수 (안전성을 위해)', default: 1 }, backup: { type: 'boolean', description: '백업 생성', default: true }, word_boundary: { type: 'boolean', description: '단어 경계 검사 (부분 매칭 방지)', default: false }, preview_only: { type: 'boolean', description: '미리보기만 (실제 편집 안함)', default: false }, case_sensitive: { type: 'boolean', description: '대소문자 구분', default: true } }, required: ['path', 'old_text', 'new_text'] } }, { name: 'fast_safe_edit', description: '안전한 스마트 편집: 위험 감지 및 대화형 확인', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '편집할 파일 경로' }, old_text: { type: 'string', description: '교체할 텍스트' }, new_text: { type: 'string', description: '새로운 텍스트' }, safety_level: { type: 'string', enum: ['strict', 'moderate', 'flexible'], default: 'moderate', description: '안전 수준 (strict: 매우 안전, moderate: 균형, flexible: 유연)' }, auto_add_context: { type: 'boolean', description: '자동 컨텍스트 추가', default: true }, require_confirmation: { type: 'boolean', description: '위험시 확인 요구', default: true } }, required: ['path', 'old_text', 'new_text'] } }, { name: 'fast_edit_multiple_blocks', description: '파일의 여러 부분을 한 번에 편집합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '편집할 파일 경로' }, edits: { type: 'array', description: '편집 작업 목록', items: { type: 'object', properties: { old_text: { type: 'string', description: '찾을 기존 텍스트' }, new_text: { type: 'string', description: '새로운 텍스트' }, line_number: { type: 'number', description: '라인 번호' }, mode: { type: 'string', enum: ['replace', 'insert_before', 'insert_after', 'delete_line'], default: 'replace' } } } }, backup: { type: 'boolean', description: '백업 생성', default: true } }, required: ['path', 'edits'] } }, { name: 'fast_edit_blocks', description: '여러개의 정교한 블록 편집을 한 번에 처리 (fast_edit_block 배열)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '편집할 파일 경로' }, edits: { type: 'array', description: '정교한 블록 편집 목록', items: { type: 'object', properties: { old_text: { type: 'string', description: '정확히 매칭할 기존 텍스트' }, new_text: { type: 'string', description: '새로운 텍스트' }, expected_replacements: { type: 'number', description: '예상 교체 횟수', default: 1 } }, required: ['old_text', 'new_text'] } }, backup: { type: 'boolean', description: '백업 생성', default: true } }, required: ['path', 'edits'] } }, { name: 'fast_extract_lines', description: '파일에서 특정 라인들을 추출합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '파일 경로' }, line_numbers: { type: 'array', items: { type: 'number' }, description: '추출할 라인 번호들' }, start_line: { type: 'number', description: '시작 라인 (범위 추출용)' }, end_line: { type: 'number', description: '끝 라인 (범위 추출용)' }, pattern: { type: 'string', description: '패턴으로 라인 추출' }, context_lines: { type: 'number', description: '패턴 매칭시 앞뒤 컨텍스트 라인 수', default: 0 } }, required: ['path'] } }, { name: 'fast_copy_file', description: '파일이나 디렉토리를 복사합니다', inputSchema: { type: 'object', properties: { source: { type: 'string', description: '원본 파일/디렉토리 경로' }, destination: { type: 'string', description: '대상 경로' }, overwrite: { type: 'boolean', description: '기존 파일 덮어쓰기', default: false }, preserve_timestamps: { type: 'boolean', description: '타임스탬프 보존', default: true }, recursive: { type: 'boolean', description: '디렉토리 재귀적 복사', default: true }, create_dirs: { type: 'boolean', description: '대상 디렉토리 자동 생성', default: true } }, required: ['source', 'destination'] } }, { name: 'fast_move_file', description: '파일이나 디렉토리를 이동하거나 이름을 변경합니다', inputSchema: { type: 'object', properties: { source: { type: 'string', description: '원본 파일/디렉토리 경로' }, destination: { type: 'string', description: '대상 경로' }, overwrite: { type: 'boolean', description: '기존 파일 덮어쓰기', default: false }, create_dirs: { type: 'boolean', description: '대상 디렉토리 자동 생성', default: true }, backup_if_exists: { type: 'boolean', description: '대상 파일이 존재할 경우 백업 생성', default: false } }, required: ['source', 'destination'] } }, { name: 'fast_delete_file', description: '파일이나 디렉토리를 삭제합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '삭제할 파일/디렉토리 경로' }, recursive: { type: 'boolean', description: '디렉토리 재귀적 삭제', default: false }, force: { type: 'boolean', description: '강제 삭제', default: false }, backup_before_delete: { type: 'boolean', description: '삭제 전 백업 생성', default: false }, confirm_delete: { type: 'boolean', description: '삭제 확인 (안전장치)', default: true } }, required: ['path'] } }, { name: 'fast_batch_file_operations', description: '여러 파일에 대한 일괄 작업을 수행합니다', inputSchema: { type: 'object', properties: { operations: { type: 'array', description: '일괄 작업 목록', items: { type: 'object', properties: { operation: { type: 'string', enum: ['copy', 'move', 'delete', 'rename'], description: '작업 유형' }, source: { type: 'string', description: '원본 경로' }, destination: { type: 'string', description: '대상 경로 (copy, move, rename용)' }, overwrite: { type: 'boolean', description: '덮어쓰기 허용', default: false } }, required: ['operation', 'source'] } }, stop_on_error: { type: 'boolean', description: '에러 발생시 중단', default: true }, dry_run: { type: 'boolean', description: '실제 실행 없이 미리보기', default: false }, create_backup: { type: 'boolean', description: '변경 전 백업 생성', default: false } }, required: ['operations'] } }, { name: 'fast_compress_files', description: '파일이나 디렉토리를 압축합니다', inputSchema: { type: 'object', properties: { paths: { type: 'array', items: { type: 'string' }, description: '압축할 파일/디렉토리 경로들' }, output_path: { type: 'string', description: '출력 압축 파일 경로' }, format: { type: 'string', enum: ['zip', 'tar', 'tar.gz', 'tar.bz2'], default: 'zip', description: '압축 형식' }, compression_level: { type: 'number', minimum: 0, maximum: 9, default: 6, description: '압축 레벨 (0=저장만, 9=최고압축)' }, exclude_patterns: { type: 'array', items: { type: 'string' }, description: '제외할 패턴들 (예: *.log, node_modules)', default: [] } }, required: ['paths', 'output_path'] } }, { name: 'fast_extract_archive', description: '압축 파일을 해제합니다', inputSchema: { type: 'object', properties: { archive_path: { type: 'string', description: '압축 파일 경로' }, extract_to: { type: 'string', description: '해제할 디렉토리', default: '.' }, overwrite: { type: 'boolean', description: '기존 파일 덮어쓰기', default: false }, create_dirs: { type: 'boolean', description: '디렉토리 자동 생성', default: true }, preserve_permissions: { type: 'boolean', description: '권한 보존', default: true }, extract_specific: { type: 'array', items: { type: 'string' }, description: '특정 파일들만 해제 (선택적)' } }, required: ['archive_path'] } }, { name: 'fast_sync_directories', description: '두 디렉토리를 동기화합니다', inputSchema: { type: 'object', properties: { source_dir: { type: 'string', description: '원본 디렉토리' }, target_dir: { type: 'string', description: '대상 디렉토리' }, sync_mode: { type: 'string', enum: ['mirror', 'update', 'merge'], default: 'update', description: '동기화 모드' }, delete_extra: { type: 'boolean', description: '대상에만 있는 파일 삭제', default: false }, preserve_newer: { type: 'boolean', description: '더 새로운 파일 보존', default: true }, dry_run: { type: 'boolean', description: '실제 실행 없이 미리보기', default: false }, exclude_patterns: { type: 'array', items: { type: 'string' }, description: '제외할 패턴들', default: ['.git', 'node_modules', '.DS_Store'] } }, required: ['source_dir', 'target_dir'] } }, ], }; }); // 툴 호출 핸들러 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 handleReadFileWithAutoChunking(args); break; case 'fast_read_multiple_files': result = await handleReadMultipleFiles(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 handleListDirectoryWithAutoChunking(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 handleSearchFilesWithAutoChunking(args); break; case 'fast_search_code': result = await handleSearchCode(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; case 'fast_edit_block': result = await handleEditBlockSafe(args); break; case 'fast_edit_blocks': result = await handleEditBlocks(args); break; case 'fast_edit_multiple_blocks': result = await handleEditMultipleBlocks(args); break; case 'fast_safe_edit': result = await handleSafeEdit(args); break; case 'fast_extract_lines': result = await handleExtractLines(args); break; case 'fast_copy_file': result = await handleCopyFile(args); break; case 'fast_move_file': result = await handleMoveFile(args); break; case 'fast_delete_file': result = await handleDeleteFile(args); break; case 'fast_batch_file_operations': result = await handleBatchFileOperations(args); break; case 'fast_compress_files': result = await handleCompressFiles(args); break; case 'fast_extract_archive': result = await handleExtractArchive(args); break; case 'fast_sync_directories': result = await handleSyncDirectories(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: getAllowedDirectories(), 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: '3.2.4', features: ['emoji-guidelines', 'large-file-writing', 'smart-recommendations', 'configurable-backup'], emoji_policy: 'Emojis not recommended in all file types', backup_enabled: CREATE_BACKUP_FILES, backup_env_var: 'MCP_CREATE_BACKUP_FILES', timestamp: new Date().toISOString() } }; } async function handleReadFile(args) { const { path: filePath, start_offset = 0, max_size, line_start, line_count, encoding = 'utf-8', continuation_token, auto_chunk = true } = 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; // 라인 모드 - Python 방식으로 스트리밍 읽기 if (line_start !== undefined) { const linesToRead = line_count ? Math.min(line_count, CLAUDE_MAX_LINES) : CLAUDE_MAX_LINES; const lines = []; // 큰 파일은 스트리밍으로 처리 if (stats.size > 10 * 1024 * 1024) { // 10MB 이상 const fileHandle = await fs.open(safePath_resolved, 'r'); const stream = fileHandle.createReadStream({ encoding: encoding }); let currentLine = 0; let buffer = ''; for await (const chunk of stream) { buffer += chunk; const chunkLines = buffer.split('\n'); buffer = chunkLines.pop() || ''; // 마지막 불완전한 라인은 보관 for (const line of chunkLines) { if (currentLine >= line_start && lines.length < linesToRead) { lines.push(line); } currentLine++; if (lines.length >= linesToRead) { break; } } if (lines.length >= linesToRead) { break; } } // 버퍼에 남은 마지막 라인 처리 if (buffer && currentLine >= line_start && lines.length < linesToRead) { lines.push(buffer); } await fileHandle.close(); } else { // 작은 파일은 기존 방식 (하지만 전체 라인 수는 세지 않음) const fileContent = await fs.readFile(safePath_resolved, encoding); const allLines = fileContent.split('\n'); const selectedLines = allLines.slice(line_start, line_start + linesToRead); lines.push(...selectedLines); } return { content: lines.join('\n'), mode: 'lines', start_line: line_start, lines_read: lines.length, file_size: stats.size, file_size_readable: formatSize(stats.size), encoding: encoding, has_more: lines.length >= linesToRead, // 요청한 만큼 읽었다면 더 있을 가능성 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 handleReadMultipleFiles(args) { const { paths = [], continuation_tokens = {}, // 파일별 continuation token auto_continue = true, // 자동으로 전체 파일 읽기 chunk_size = 1024 * 1024 // 1MB 청크 크기 } = args; if (!Array.isArray(paths) || paths.length === 0) { throw new Error('paths parameter must be a non-empty array'); } const results = []; const errors = []; const continuationData = {}; let totalSuccessful = 0; let totalErrors = 0; // 각 파일을 병렬로 읽기 const readPromises = paths.map(async (filePath, index) => { try { const safePath_resolved = safePath(filePath); const stats = await fs.stat(safePath_resolved); if (!stats.isFile()) { throw new Error('Path is not a file'); } // 이미지 파일 처리 const ext = path.extname(safePath_resolved).toLowerCase(); const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg']; if (imageExtensions.includes(ext)) { return { path: safePath_resolved, name: path.basename(safePath_resolved), type: 'image', content: '[IMAGE FILE - Content not displayed]', size: stats.size, size_readable: formatSize(stats.size), modified: stats.mtime.toISOString(), extension: ext, mime_type: getMimeType(safePath_resolved), encoding: 'binary', index: index }; } // 기존 continuation token 확인 const existingToken = continuation_tokens[safePath_resolved]; let startOffset = existingToken ? existingToken.next_offset : 0; // 텍스트 파일 읽기 (청킹 지원) let content = ''; let totalBytesRead = 0; let hasMore = false; let nextOffset = startOffset; if (auto_continue) { // 자동으로 전체 파일 읽기 (여러 청크) const fileHandle = await fs.open(safePath_resolved, 'r'); try { while (nextOffset < stats.size) { const remainingBytes = stats.size - nextOffset; const currentChunkSize = Math.min(chunk_size, remainingBytes); const buffer = Buffer.alloc(currentChunkSize); const { bytesRead } = await fileHandle.read(buffer, 0, currentChunkSize, nextOffset); if (bytesRead === 0) break; const chunkContent = buffer.subarray(0, bytesRead).toString('utf-8'); content += chunkContent; totalBytesRead += bytesRead; nextOffset += bytesRead; // 매우 큰 파일의 경우 일정 크기에서 중단 (5MB 제한) if (totalBytesRead >= 5 * 1024 * 1024) { hasMore = nextOffset < stats.size; break; } } } finally { await fileHandle.close(); } } else { // 단일 청크만 읽기 const fileHandle = await fs.open(safePath_resolved, 'r'); const buffer = Buffer.alloc(chunk_size); const { bytesRead } = await fileHandle.read(buffer, 0, chunk_size, startOffset); await fileHandle.close(); content = buffer.subarray(0, bytesRead).toString('utf-8'); totalBytesRead = bytesRead; nextOffset = startOffset + bytesRead; hasMore = nextOffset < stats.size; } // Continuation token 생성 (더 읽을 내용이 있는 경우) let continuationToken = null; if (hasMore) { continuationToken = { file_path: safePath_resolved, next_offset: nextOffset, total_size: stats.size, read_so_far: nextOffset, chunk_size: chunk_size, progress_percent: ((nextOffset / stats.size) * 100).toFixed(2) + '%' }; continuationData[safePath_resolved] = continuationToken; } return { path: safePath_resolved, name: path.basename(safePath_resolved), type: 'text', content: content, size: stats.size, size_readable: formatSize(stats.size), modified: stats.mtime.toISOString(), created: stats.birthtime.toISOString(), extension: ext, mime_type: getMimeType(safePath_resolved), encoding: 'utf-8', bytes_read: totalBytesRead, start_offset: startOffset, end_offset: nextOffset, is_complete: !hasMore, has_more: hasMore, continuation_token: continuationToken, auto_continued: auto_continue && startOffset === 0, index: index }; } catch (error) { return { path: filePath, name: path.basename(filePath), type: 'error', content: null, error: error instanceof Error ? error.message : 'Unknown error', index: index }; } }); // 모든 파일 읽기 완료 대기 const fileResults = await Promise.all(readPromises); // 결과 분류 fileResults.forEach(result => { if (result.type === 'error') { errors.push(result); totalErrors++; } else { results.push(result); totalSuccessful++; } }); // 결과를 원래 순서대로 정렬 results.sort((a, b) => a.index - b.index); errors.sort((a, b) => a.index - b.index); // 통계 계산 const incompleteFiles = results.filter(r => r.has_more); const completedFiles = results.filter(r => r.is_complete); return { message: 'Multiple files read completed', total_files: paths.length, successful: totalSuccessful, errors: totalErrors, completed_files: completedFiles.length, incomplete_files: incompleteFiles.length, files: results, failed_files: errors, continuation_data: Object.keys(continuationData).length > 0 ? continuationData : null, continuation_guide: incompleteFiles.length > 0 ? { message: "Some files were not fully read", next_request_example: { paths: incompleteFiles.map(f => f.path), continuation_tokens: continuationData, auto_continue: false }, tip: "Set auto_continue: false to read files in smaller chunks" } : null, performance: { parallel_read: true, chunk_size_mb: chunk_size / (1024 * 1024), auto_continue_enabled: auto_continue, max_file_size_limit_mb: auto_continue ? 5 : 1 }, timestamp: new Date().toISOString() }; } async function handleWriteFile(args) { const { path: filePath, content, encoding = 'utf-8', create_dirs = true, append = false, force_remove_emojis = 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); // 백업 생성 (설정에 따라) let backupPath = null; if (CREATE_BACKUP_FILES && !append) { try { await fs.access(resolvedPath); backupPath = `${resolvedPath}.backup.${Date.now()}`; await fs.copyFile(resolvedPath, backupPath); } catch { // 원본 파일이 없으면 백업 생성 안함 } } if (create_dirs) { const dir = path.dirname(resolvedPath); await fs.mkdir(dir, { recursive: true }); }