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

204 lines 7.02 kB
// 자동 청킹 시스템 // 응답 크기가 1MB를 넘기 전에 자동으로 분할하는 기능 export class ResponseSizeMonitor { currentSize = 0; maxSize; warningThreshold; constructor(maxSizeMB = 0.9) { this.maxSize = maxSizeMB * 1024 * 1024; this.warningThreshold = this.maxSize * 0.85; // 85%에서 경고 } reset() { this.currentSize = 0; } estimateSize(obj) { // JSON 직렬화 크기 추정 (오버헤드 포함) const jsonStr = JSON.stringify(obj); return Buffer.byteLength(jsonStr, 'utf8') * 1.2; // 20% 마진 } addContent(content) { const contentSize = this.estimateSize(content); this.currentSize += contentSize; return this.currentSize < this.maxSize; } canAddContent(content) { const contentSize = this.estimateSize(content); return (this.currentSize + contentSize) < this.maxSize; } isNearLimit() { return this.currentSize > this.warningThreshold; } getCurrentSize() { return this.currentSize; } getMaxSize() { return this.maxSize; } getRemainingSize() { return Math.max(0, this.maxSize - this.currentSize); } getSizeInfo() { return { current_size: this.currentSize, max_size: this.maxSize, remaining_size: this.getRemainingSize(), usage_percentage: (this.currentSize / this.maxSize) * 100, is_near_limit: this.isNearLimit() }; } } export class ContinuationTokenManager { tokens = new Map(); TOKEN_EXPIRY_MS = 30 * 60 * 1000; // 30분 generateToken(type, path, params) { const tokenId = `${type}_${Date.now()}_${Math.random().toString(36).substring(2)}`; const token = { type, path, chunk_id: tokenId, timestamp: Date.now(), params: { ...params } }; this.tokens.set(tokenId, token); this.cleanupExpiredTokens(); return tokenId; } getToken(tokenId) { const token = this.tokens.get(tokenId); if (!token) return null; // 토큰 만료 확인 if (Date.now() - token.timestamp > this.TOKEN_EXPIRY_MS) { this.tokens.delete(tokenId); return null; } return token; } updateToken(tokenId, updates) { const token = this.tokens.get(tokenId); if (!token) return false; Object.assign(token, updates); return true; } deleteToken(tokenId) { return this.tokens.delete(tokenId); } cleanupExpiredTokens() { const now = Date.now(); for (const [tokenId, token] of this.tokens.entries()) { if (now - token.timestamp > this.TOKEN_EXPIRY_MS) { this.tokens.delete(tokenId); } } } getActiveTokenCount() { this.cleanupExpiredTokens(); return this.tokens.size; } } // 자동 청킹 헬퍼 함수들 export class AutoChunkingHelper { // 배열을 응답 크기 제한에 맞게 자동 분할 static chunkArray(items, monitor, estimateItemSize) { const chunks = []; let i = 0; while (i < items.length) { const item = items[i]; const estimatedItem = estimateItemSize(item); if (!monitor.canAddContent(estimatedItem)) { break; } monitor.addContent(estimatedItem); chunks.push(item); i++; } return { chunks, hasMore: i < items.length, remainingItems: items.slice(i) }; } // 텍스트를 라인 단위로 안전하게 분할 static chunkTextByLines(text, monitor, startLine = 0) { const lines = text.split('\n'); const contentLines = []; let currentLine = startLine; while (currentLine < lines.length) { const line = lines[currentLine] + '\n'; if (!monitor.canAddContent({ line })) { break; } monitor.addContent({ line }); contentLines.push(lines[currentLine]); currentLine++; } return { content: contentLines.join('\n'), hasMore: currentLine < lines.length, nextStartLine: currentLine, totalLines: lines.length }; } // 바이트 단위로 안전하게 분할 static chunkByBytes(buffer, monitor, startOffset = 0, encoding = 'utf-8') { let currentOffset = startOffset; let bytesRead = 0; const maxChunkSize = Math.min(monitor.getRemainingSize() / 2, buffer.length - startOffset); // 안전한 텍스트 경계 찾기 (UTF-8 고려) let safeEndOffset = startOffset; const testChunkSize = Math.min(4096, maxChunkSize); // 4KB씩 테스트 while (safeEndOffset < buffer.length && (safeEndOffset - startOffset) < maxChunkSize) { const nextOffset = Math.min(safeEndOffset + testChunkSize, buffer.length); const chunk = buffer.subarray(startOffset, nextOffset); try { const content = chunk.toString(encoding); const contentObj = { content }; if (!monitor.canAddContent(contentObj)) { break; } safeEndOffset = nextOffset; bytesRead = nextOffset - startOffset; } catch (error) { // UTF-8 디코딩 오류 - 이전 안전한 위치로 롤백 break; } } const finalChunk = buffer.subarray(startOffset, safeEndOffset); const content = finalChunk.toString(encoding); monitor.addContent({ content }); return { content, hasMore: safeEndOffset < buffer.length, nextOffset: safeEndOffset, bytesRead }; } } export function createChunkedResponse(data, hasMore, monitor, continuationToken, chunkInfo) { const response = { ...data, chunking: { has_more: hasMore, chunk_info: { current_chunk: chunkInfo?.current || 1, estimated_total_chunks: chunkInfo?.total, progress_percentage: chunkInfo?.total ? ((chunkInfo.current / chunkInfo.total) * 100) : undefined }, size_info: { current_size: monitor.getCurrentSize(), max_size: monitor.getMaxSize(), usage_percentage: (monitor.getCurrentSize() / monitor.getMaxSize()) * 100 } } }; if (hasMore && continuationToken) { response.chunking.continuation_token = continuationToken; } return response; } // 전역 인스턴스 export const globalTokenManager = new ContinuationTokenManager(); //# sourceMappingURL=auto-chunking.js.map