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
JavaScript
// 자동 청킹 시스템
// 응답 크기가 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