Skip to main content
Glama
filesystem_tools.js23.2 kB
/** * Filesystem tools for MCP * Provides secure file system operations through the Model Context Protocol */ const fs = require('fs'); const path = require('path'); const logger = require('./logger'); const mime = require('mime-types'); const { globSync } = require('glob'); // Maximum file size for reading (10MB default) const MAX_FILE_SIZE = 10 * 1024 * 1024; /** * Validates that a path is within allowed directories * @param {string} filePath - Path to validate * @param {Array<string>} allowedDirs - List of allowed directories * @returns {boolean} - Whether the path is allowed */ function isPathAllowed(filePath, allowedDirs) { if (!allowedDirs || !Array.isArray(allowedDirs) || allowedDirs.length === 0) { logger.error('No allowed directories configured'); return false; } // Resolve to absolute path const resolvedPath = path.resolve(filePath); // Check if the path is within any of the allowed directories return allowedDirs.some(dir => { const resolvedDir = path.resolve(dir); return resolvedPath === resolvedDir || resolvedPath.startsWith(resolvedDir + path.sep); }); } /** * Reads a file and returns its contents * @param {string} filePath - Path to the file * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object with file content */ function readFile(filePath, allowedDirs) { try { if (!filePath) { return { success: false, message: 'No file path provided' }; } if (!isPathAllowed(filePath, allowedDirs)) { return { success: false, message: `Access to path "${filePath}" is not allowed` }; } const resolvedPath = path.resolve(filePath); // Check if file exists if (!fs.existsSync(resolvedPath)) { return { success: false, message: `File not found: ${filePath}` }; } // Check if it's a directory const stats = fs.statSync(resolvedPath); if (stats.isDirectory()) { return { success: false, message: `Path is a directory, not a file: ${filePath}` }; } // Check file size if (stats.size > MAX_FILE_SIZE) { return { success: false, message: `File is too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Maximum allowed size is ${MAX_FILE_SIZE / 1024 / 1024}MB.` }; } // Determine file type const mimeType = mime.lookup(resolvedPath) || 'application/octet-stream'; const isText = mimeType.startsWith('text/') || ['application/json', 'application/javascript', 'application/xml'].includes(mimeType); // Read file based on type let content; if (isText) { content = fs.readFileSync(resolvedPath, 'utf8'); } else { // For binary files, return base64 encoded data const buffer = fs.readFileSync(resolvedPath); content = buffer.toString('base64'); } return { success: true, content, mimeType, isText, size: stats.size, path: filePath }; } catch (error) { logger.error(`Error reading file: ${error.message}`); return { success: false, message: `Error reading file: ${error.message}` }; } } /** * Reads multiple files at once * @param {Array<string>} filePaths - List of file paths to read * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object with file contents */ function readMultipleFiles(filePaths, allowedDirs) { try { if (!filePaths || !Array.isArray(filePaths) || filePaths.length === 0) { return { success: false, message: 'No file paths provided' }; } const results = filePaths.map(filePath => { const result = readFile(filePath, allowedDirs); return { path: filePath, success: result.success, content: result.success ? result.content : null, mimeType: result.mimeType, isText: result.isText, error: result.success ? null : result.message }; }); return { success: true, results }; } catch (error) { logger.error(`Error reading multiple files: ${error.message}`); return { success: false, message: `Error reading multiple files: ${error.message}` }; } } /** * Writes content to a file * @param {string} filePath - Path to the file * @param {string} content - Content to write * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object */ function writeFile(filePath, content, allowedDirs) { try { if (!filePath) { return { success: false, message: 'No file path provided' }; } if (!content && content !== '') { return { success: false, message: 'No content provided' }; } if (!isPathAllowed(filePath, allowedDirs)) { return { success: false, message: `Access to path "${filePath}" is not allowed` }; } const resolvedPath = path.resolve(filePath); // Ensure the directory exists const directory = path.dirname(resolvedPath); if (!fs.existsSync(directory)) { fs.mkdirSync(directory, { recursive: true }); } // Write the file fs.writeFileSync(resolvedPath, content); return { success: true, message: `File written successfully: ${filePath}`, path: filePath }; } catch (error) { logger.error(`Error writing file: ${error.message}`); return { success: false, message: `Error writing file: ${error.message}` }; } } /** * Lists files and directories in a directory * @param {string} dirPath - Path to the directory * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object with directory contents */ function listDirectory(dirPath, allowedDirs) { try { if (!dirPath) { return { success: false, message: 'No directory path provided' }; } if (!isPathAllowed(dirPath, allowedDirs)) { return { success: false, message: `Access to path "${dirPath}" is not allowed` }; } const resolvedPath = path.resolve(dirPath); // Check if directory exists if (!fs.existsSync(resolvedPath)) { return { success: false, message: `Directory not found: ${dirPath}` }; } // Check if it's a directory const stats = fs.statSync(resolvedPath); if (!stats.isDirectory()) { return { success: false, message: `Path is not a directory: ${dirPath}` }; } // Read directory contents const items = fs.readdirSync(resolvedPath); // Get information about each item const contents = items.map(item => { const itemPath = path.join(resolvedPath, item); const itemStats = fs.statSync(itemPath); const type = itemStats.isDirectory() ? 'directory' : 'file'; return { name: item, type, path: path.join(dirPath, item), size: itemStats.size, modified: itemStats.mtime.toISOString() }; }); // Create content string for compatibility const contentLines = contents.map(item => `${item.type === 'directory' ? '[DIR]' : '[FILE]'} ${item.name} (${item.size} bytes, modified: ${item.modified})` ); const content = `Directory listing for ${dirPath}:\n${contentLines.join('\n')}`; return { success: true, path: dirPath, contents, content // Add content field for compatibility }; } catch (error) { logger.error(`Error listing directory: ${error.message}`); return { success: false, message: `Error listing directory: ${error.message}` }; } } /** * Creates a directory * @param {string} dirPath - Path to the directory * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object */ function createDirectory(dirPath, allowedDirs) { try { if (!dirPath) { return { success: false, message: 'No directory path provided' }; } if (!isPathAllowed(dirPath, allowedDirs)) { return { success: false, message: `Access to path "${dirPath}" is not allowed` }; } const resolvedPath = path.resolve(dirPath); // Create the directory fs.mkdirSync(resolvedPath, { recursive: true }); return { success: true, message: `Directory created successfully: ${dirPath}`, path: dirPath }; } catch (error) { logger.error(`Error creating directory: ${error.message}`); return { success: false, message: `Error creating directory: ${error.message}` }; } } /** * Moves or renames a file or directory * @param {string} sourcePath - Source path * @param {string} destinationPath - Destination path * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object */ function moveFile(sourcePath, destinationPath, allowedDirs) { try { if (!sourcePath) { return { success: false, message: 'No source path provided' }; } if (!destinationPath) { return { success: false, message: 'No destination path provided' }; } if (!isPathAllowed(sourcePath, allowedDirs) || !isPathAllowed(destinationPath, allowedDirs)) { return { success: false, message: 'Access to source or destination path is not allowed' }; } const resolvedSourcePath = path.resolve(sourcePath); const resolvedDestPath = path.resolve(destinationPath); // Check if source exists if (!fs.existsSync(resolvedSourcePath)) { return { success: false, message: `Source not found: ${sourcePath}` }; } // Check if destination exists if (fs.existsSync(resolvedDestPath)) { return { success: false, message: `Destination already exists: ${destinationPath}` }; } // Ensure destination directory exists const destDir = path.dirname(resolvedDestPath); if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, { recursive: true }); } // Move/rename the file or directory fs.renameSync(resolvedSourcePath, resolvedDestPath); return { success: true, message: `Successfully moved ${sourcePath} to ${destinationPath}`, source: sourcePath, destination: destinationPath }; } catch (error) { logger.error(`Error moving file: ${error.message}`); return { success: false, message: `Error moving file: ${error.message}` }; } } /** * Searches for files and directories matching a pattern * @param {string} searchPath - Path to search in * @param {string} pattern - Search pattern * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object with search results */ function searchFiles(searchPath, pattern, allowedDirs) { try { if (!searchPath) { return { success: false, message: 'No search path provided' }; } if (!pattern) { return { success: false, message: 'No search pattern provided' }; } if (!isPathAllowed(searchPath, allowedDirs)) { return { success: false, message: `Access to path "${searchPath}" is not allowed` }; } const resolvedPath = path.resolve(searchPath); // Check if directory exists if (!fs.existsSync(resolvedPath)) { return { success: false, message: `Directory not found: ${searchPath}` }; } // Check if it's a directory const stats = fs.statSync(resolvedPath); if (!stats.isDirectory()) { return { success: false, message: `Path is not a directory: ${searchPath}` }; } // Use glob to search for files const searchPattern = path.join(resolvedPath, '**', pattern); const matches = globSync(searchPattern, { nodir: false }); // Convert absolute paths back to relative paths const results = matches.map(match => ({ path: path.relative(process.cwd(), match), type: fs.statSync(match).isDirectory() ? 'directory' : 'file' })); // Create content string for compatibility const content = results.length > 0 ? `Found ${results.length} files matching "${pattern}":\n${results.map(r => `${r.type === 'directory' ? '[DIR]' : '[FILE]'} ${r.path}`).join('\n')}` : `No files found matching "${pattern}" in ${searchPath}`; return { success: true, results, count: results.length, pattern, content // Add content field for compatibility }; } catch (error) { logger.error(`Error searching files: ${error.message}`); return { success: false, message: `Error searching files: ${error.message}` }; } } /** * Gets information about a file or directory * @param {string} filePath - Path to the file or directory * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object with file information */ function getFileInfo(filePath, allowedDirs) { try { if (!filePath) { return { success: false, message: 'No file path provided' }; } if (!isPathAllowed(filePath, allowedDirs)) { return { success: false, message: `Access to path "${filePath}" is not allowed` }; } const resolvedPath = path.resolve(filePath); // Check if file exists if (!fs.existsSync(resolvedPath)) { return { success: false, message: `File not found: ${filePath}` }; } // Get file stats const stats = fs.statSync(resolvedPath); const type = stats.isDirectory() ? 'directory' : 'file'; // Determine MIME type for files let mimeType = null; if (type === 'file') { mimeType = mime.lookup(resolvedPath) || 'application/octet-stream'; } return { success: true, path: filePath, type, size: stats.size, created: stats.birthtime.toISOString(), modified: stats.mtime.toISOString(), accessed: stats.atime.toISOString(), mimeType, permissions: { readable: (() => { try { fs.accessSync(resolvedPath, fs.constants.R_OK); return true; } catch { return false; } })(), writable: (() => { try { fs.accessSync(resolvedPath, fs.constants.W_OK); return true; } catch { return false; } })(), executable: (() => { try { fs.accessSync(resolvedPath, fs.constants.X_OK); return true; } catch { return false; } })() } }; } catch (error) { logger.error(`Error getting file info: ${error.message}`); return { success: false, message: `Error getting file info: ${error.message}` }; } } /** * Copies a file or directory * @param {string} sourcePath - Source path * @param {string} destinationPath - Destination path * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object */ function copyFile(sourcePath, destinationPath, allowedDirs) { try { if (!sourcePath) { return { success: false, message: 'No source path provided' }; } if (!destinationPath) { return { success: false, message: 'No destination path provided' }; } if (!isPathAllowed(sourcePath, allowedDirs) || !isPathAllowed(destinationPath, allowedDirs)) { return { success: false, message: 'Access to source or destination path is not allowed' }; } const resolvedSourcePath = path.resolve(sourcePath); const resolvedDestPath = path.resolve(destinationPath); // Check if source exists if (!fs.existsSync(resolvedSourcePath)) { return { success: false, message: `Source not found: ${sourcePath}` }; } // Check if source is a directory or file const stats = fs.statSync(resolvedSourcePath); if (stats.isDirectory()) { // Create destination directory if it doesn't exist if (!fs.existsSync(resolvedDestPath)) { fs.mkdirSync(resolvedDestPath, { recursive: true }); } // Copy directory contents recursively const items = fs.readdirSync(resolvedSourcePath); for (const item of items) { const srcItemPath = path.join(resolvedSourcePath, item); const destItemPath = path.join(resolvedDestPath, item); if (fs.statSync(srcItemPath).isDirectory()) { // Recursive call for subdirectories copyFile(srcItemPath, destItemPath, allowedDirs); } else { // Copy file fs.copyFileSync(srcItemPath, destItemPath); } } } else { // Ensure destination directory exists const destDir = path.dirname(resolvedDestPath); if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, { recursive: true }); } // Copy file fs.copyFileSync(resolvedSourcePath, resolvedDestPath); } return { success: true, message: `Successfully copied ${sourcePath} to ${destinationPath}`, source: sourcePath, destination: destinationPath, type: stats.isDirectory() ? 'directory' : 'file' }; } catch (error) { logger.error(`Error copying file: ${error.message}`); return { success: false, message: `Error copying file: ${error.message}` }; } } /** * Deletes a file or directory * @param {string} filePath - Path to the file or directory * @param {boolean} recursive - Whether to delete directories recursively * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object */ function deleteFile(filePath, recursive, allowedDirs) { try { if (!filePath) { return { success: false, message: 'No file path provided' }; } if (!isPathAllowed(filePath, allowedDirs)) { return { success: false, message: `Access to path "${filePath}" is not allowed` }; } const resolvedPath = path.resolve(filePath); // Check if file exists if (!fs.existsSync(resolvedPath)) { return { success: false, message: `File not found: ${filePath}` }; } // Check if it's a directory const stats = fs.statSync(resolvedPath); if (stats.isDirectory()) { if (recursive) { // Recursively delete directory fs.rmSync(resolvedPath, { recursive: true, force: true }); } else { // Check if directory is empty const items = fs.readdirSync(resolvedPath); if (items.length > 0) { return { success: false, message: `Directory is not empty: ${filePath}. Use recursive=true to delete non-empty directories.` }; } // Delete empty directory fs.rmdirSync(resolvedPath); } } else { // Delete file fs.unlinkSync(resolvedPath); } return { success: true, message: `Successfully deleted ${stats.isDirectory() ? 'directory' : 'file'}: ${filePath}`, path: filePath, type: stats.isDirectory() ? 'directory' : 'file' }; } catch (error) { logger.error(`Error deleting file: ${error.message}`); return { success: false, message: `Error deleting file: ${error.message}` }; } } /** * Creates a tree representation of a directory * @param {string} dirPath - Path to the directory * @param {number} depth - Maximum depth to traverse * @param {boolean} followSymlinks - Whether to follow symbolic links * @param {Array<string>} allowedDirs - List of allowed directories * @returns {object} - Response object with directory tree */ function createDirectoryTree(dirPath, depth = 3, followSymlinks = false, allowedDirs) { try { if (!dirPath) { return { success: false, message: 'No directory path provided' }; } if (!isPathAllowed(dirPath, allowedDirs)) { return { success: false, message: `Access to path "${dirPath}" is not allowed` }; } const resolvedPath = path.resolve(dirPath); // Check if directory exists if (!fs.existsSync(resolvedPath)) { return { success: false, message: `Directory not found: ${dirPath}` }; } // Check if it's a directory const stats = fs.statSync(resolvedPath); if (!stats.isDirectory()) { return { success: false, message: `Path is not a directory: ${dirPath}` }; } // Function to recursively build tree function buildTree(currentPath, currentDepth) { if (currentDepth > depth) { return null; } const name = path.basename(currentPath); const stats = fs.statSync(currentPath); // Handle symlinks let isSymlink = false; let symlinkTarget = null; try { isSymlink = fs.lstatSync(currentPath).isSymbolicLink(); if (isSymlink) { symlinkTarget = fs.readlinkSync(currentPath); // Don't follow symlinks if not allowed or if they point outside allowed dirs if (!followSymlinks || !isPathAllowed(symlinkTarget, allowedDirs)) { return { name, type: 'symlink', target: symlinkTarget, followed: false }; } // Use target path for further operations if following symlinks if (followSymlinks) { currentPath = symlinkTarget; } } } catch (error) { // Ignore errors from symlink checking } if (stats.isDirectory()) { // Read directory contents let items; try { items = fs.readdirSync(currentPath); } catch (error) { return { name, type: 'directory', error: `Cannot read directory: ${error.message}` }; } // Build tree for each item const children = items.map(item => { const itemPath = path.join(currentPath, item); return buildTree(itemPath, currentDepth + 1); }).filter(Boolean); return { name, type: 'directory', size: stats.size, modified: stats.mtime.toISOString(), children, isSymlink, ...(isSymlink && { symlinkTarget }) }; } else { // File node return { name, type: 'file', size: stats.size, modified: stats.mtime.toISOString(), mimeType: mime.lookup(currentPath) || 'application/octet-stream', isSymlink, ...(isSymlink && { symlinkTarget }) }; } } // Build the tree const tree = buildTree(resolvedPath, 1); return { success: true, path: dirPath, tree }; } catch (error) { logger.error(`Error creating directory tree: ${error.message}`); return { success: false, message: `Error creating directory tree: ${error.message}` }; } } /** * Lists all allowed directories * @param {string[]} allowedDirectories - List of allowed directory paths * @returns {object} - Success message with list of allowed directories */ function listAllowedDirectories(allowedDirectories) { return { success: true, message: 'Allowed directories listed successfully', directories: allowedDirectories }; } module.exports = { readFile, readMultipleFiles, writeFile, listDirectory, createDirectory, moveFile, searchFiles, getFileInfo, copyFile, deleteFile, createDirectoryTree, listAllowedDirectories };

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/FutureAtoms/agentic-control-framework'

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