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
JavaScript
#!/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 });
}