Skip to main content
Glama
fs-utils.ts4.09 kB
import { promises as fs } from 'fs'; import path from 'path'; import { FileSystemItem } from './types.js'; import { ResourceManager } from './resource-manager.js'; export function normalizeAndValidatePath(rootDir: string, requestedPath: string): string { // Reject obvious traversal patterns before resolution const dangerousPatterns = [ /\.\.[\/\\]/, // ../ or ..\ /^\.\.$/, // Just .. /^\.\.\//, // Starts with ../ /\x00/, // Null bytes /^[a-zA-Z]:[\\\/]/, // Windows absolute paths like C:\ or C:/ /^[\/\\]/, // Absolute paths starting with / or \ /%2e%2e/i, // URL encoded dots /\.\.\./, // Three or more dots /[\/\\]\.\.[\/\\]/ // /../ or \..\ ]; for (const pattern of dangerousPatterns) { if (pattern.test(requestedPath)) { throw new Error('Path traversal detected: Access outside root directory is not allowed'); } } const normalizedRoot = path.resolve(rootDir); const resolvedPath = path.resolve(normalizedRoot, requestedPath); // Double-check after resolution if (!resolvedPath.startsWith(normalizedRoot)) { throw new Error('Path traversal detected: Access outside root directory is not allowed'); } // No need for additional .. check since we already validated the resolved path return resolvedPath; } export async function listDirectory( rootDir: string, relativePath: string, recursive: boolean = true, maxDepth: number = 10, currentDepth: number = 0, resourceManager?: ResourceManager ): Promise<FileSystemItem[]> { const fullPath = normalizeAndValidatePath(rootDir, relativePath); try { const entries = await fs.readdir(fullPath, { withFileTypes: true }); const items: FileSystemItem[] = []; for (const entry of entries) { const itemPath = path.join(fullPath, entry.name); const relativeItemPath = path.relative(rootDir, itemPath); try { const stats = await fs.stat(itemPath); // Track size for resource management if (resourceManager && stats.size) { resourceManager.trackSize(stats.size); } const item: FileSystemItem = { name: entry.name, path: `/${relativeItemPath}`, type: entry.isDirectory() ? 'directory' : 'file', size: stats.size, modified: stats.mtime }; // Recursively list subdirectories if enabled and within depth limit if (recursive && entry.isDirectory() && currentDepth < maxDepth) { item.children = await listDirectory( rootDir, relativeItemPath, recursive, maxDepth, currentDepth + 1, resourceManager ); } items.push(item); } catch (error) { // Handle permission errors or inaccessible files items.push({ name: entry.name, path: `/${relativeItemPath}`, type: entry.isDirectory() ? 'directory' : 'file' }); } } return items; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { throw new Error('Directory not found'); } if ((error as NodeJS.ErrnoException).code === 'ENOTDIR') { throw new Error(`Not a directory: ${relativePath}`); } throw error; } } export async function readFileContent(filePath: string, resourceManager?: ResourceManager): Promise<string> { try { // Check file size before reading if (resourceManager) { const stats = await fs.stat(filePath); resourceManager.checkFileSize(stats.size, filePath); } const content = await fs.readFile(filePath, 'utf-8'); return content; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { throw new Error('File not found'); } if ((error as NodeJS.ErrnoException).code === 'EISDIR') { throw new Error(`Cannot read directory as file: ${filePath}`); } throw error; } }

Latest Blog Posts

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/davstr1/peekabooMCP'

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