Skip to main content
Glama

MCP Smart Filesystem Server

by lofcz
lib.ts7.7 kB
import fs from "fs/promises"; import path from "path"; import { normalizePath, expandHome } from './path-utils.js'; import { isPathWithinAllowedDirectories } from './path-validation.js'; // Global allowed directories - set by the main module let allowedDirectories: string[] = []; // Function to set allowed directories from the main module export function setAllowedDirectories(directories: string[]): void { allowedDirectories = [...directories]; } // Function to get current allowed directories export function getAllowedDirectories(): string[] { return [...allowedDirectories]; } // Type definitions export interface FileInfo { size: number; created: Date; modified: Date; accessed: Date; isDirectory: boolean; isFile: boolean; permissions: string; } // Pure Utility Functions export function formatSize(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) return '0 B'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); if (i < 0 || i === 0) return `${bytes} ${units[0]}`; const unitIndex = Math.min(i, units.length - 1); return `${(bytes / Math.pow(1024, unitIndex)).toFixed(2)} ${units[unitIndex]}`; } // Security & Validation Functions export async function validatePath(requestedPath: string): Promise<string> { const expandedPath = expandHome(requestedPath); // For relative paths, resolve against the first allowed directory const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(allowedDirectories[0] || process.cwd(), expandedPath); const normalizedRequested = normalizePath(absolute); // Security: Check if path is within allowed directories before any file operations const isAllowed = isPathWithinAllowedDirectories(normalizedRequested, allowedDirectories); if (!isAllowed) { throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`); } // Security: Handle symlinks by checking their real path to prevent symlink attacks // This prevents attackers from creating symlinks that point outside allowed directories try { const realPath = await fs.realpath(absolute); const normalizedReal = normalizePath(realPath); if (!isPathWithinAllowedDirectories(normalizedReal, allowedDirectories)) { throw new Error(`Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}`); } return realPath; } catch (error) { // Security: For new files that don't exist yet, verify parent directory // This ensures we can't create files in unauthorized locations if ((error as NodeJS.ErrnoException).code === 'ENOENT') { const parentDir = path.dirname(absolute); try { const realParentPath = await fs.realpath(parentDir); const normalizedParent = normalizePath(realParentPath); if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirectories)) { throw new Error(`Access denied - parent directory outside allowed directories: ${realParentPath} not in ${allowedDirectories.join(', ')}`); } return absolute; } catch { throw new Error(`Parent directory does not exist: ${parentDir}`); } } throw error; } } // File Operations export async function getFileStats(filePath: string): Promise<FileInfo> { const stats = await fs.stat(filePath); return { size: stats.size, created: stats.birthtime, modified: stats.mtime, accessed: stats.atime, isDirectory: stats.isDirectory(), isFile: stats.isFile(), permissions: stats.mode.toString(8).slice(-3), }; } export async function readFileContent(filePath: string, encoding: string = 'utf-8'): Promise<string> { return await fs.readFile(filePath, encoding as BufferEncoding); } // Line counting utility export function countLines(content: string): number { if (content.length === 0) return 0; let lines = 1; for (let i = 0; i < content.length; i++) { if (content[i] === '\n') lines++; } return lines; } // Detect likely language from file extension export function detectLanguage(filePath: string): string { const ext = path.extname(filePath).toLowerCase(); const langMap: Record<string, string> = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.cs': 'csharp', '.java': 'java', '.cpp': 'cpp', '.c': 'c', '.h': 'c', '.hpp': 'cpp', '.go': 'go', '.rs': 'rust', '.rb': 'ruby', '.php': 'php', '.swift': 'swift', '.kt': 'kotlin', '.scala': 'scala', '.sh': 'shell', '.bash': 'shell', '.json': 'json', '.xml': 'xml', '.html': 'html', '.css': 'css', '.sql': 'sql', '.md': 'markdown', '.yaml': 'yaml', '.yml': 'yaml', }; return langMap[ext] || 'text'; } // Check if file is likely binary export async function isBinaryFile(filePath: string): Promise<boolean> { try { const buffer = Buffer.alloc(512); const fd = await fs.open(filePath, 'r'); try { const { bytesRead } = await fd.read(buffer, 0, 512, 0); // Check for null bytes which indicate binary content for (let i = 0; i < bytesRead; i++) { if (buffer[i] === 0) { return true; } } return false; } finally { await fd.close(); } } catch { return false; } } // List directory with metadata export interface DirectoryEntry { name: string; type: 'file' | 'directory'; size: string | null; lines?: number; itemCount?: number; } export interface DirectoryListing { path: string; entries: DirectoryEntry[]; summary: { totalFiles: number; totalDirectories: number; totalSize: string; }; } export async function listDirectory(dirPath: string): Promise<DirectoryListing> { const entries = await fs.readdir(dirPath, { withFileTypes: true }); const result: DirectoryEntry[] = []; let totalFiles = 0; let totalDirs = 0; let totalBytes = 0; for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { totalDirs++; try { const subEntries = await fs.readdir(fullPath); result.push({ name: entry.name, type: 'directory', size: null, itemCount: subEntries.length }); } catch { result.push({ name: entry.name, type: 'directory', size: null, itemCount: 0 }); } } else if (entry.isFile()) { totalFiles++; try { const stats = await fs.stat(fullPath); totalBytes += stats.size; // For text files, try to count lines let lines: number | undefined; if (stats.size < 10 * 1024 * 1024) { // Only for files < 10MB const isBinary = await isBinaryFile(fullPath); if (!isBinary) { try { const content = await readFileContent(fullPath); lines = countLines(content); } catch { // Ignore errors reading file content } } } result.push({ name: entry.name, type: 'file', size: formatSize(stats.size), lines }); } catch { result.push({ name: entry.name, type: 'file', size: '0 B' }); } } } return { path: dirPath, entries: result, summary: { totalFiles, totalDirectories: totalDirs, totalSize: formatSize(totalBytes) } }; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lofcz/mcp-filesystem-smart'

If you have feedback or need assistance with the MCP directory API, please join our Discord server