UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

163 lines (159 loc) 6.2 kB
import fs from "fs/promises"; import path from "path"; import os from 'os'; // Store allowed directories - temporarily allowing all paths // TODO: Make this configurable through a configuration file const allowedDirectories = [ "/" // Root directory - effectively allows all paths ]; // Original implementation commented out for future reference /* const allowedDirectories: string[] = [ process.cwd(), // Current working directory os.homedir() // User's home directory ]; */ // Normalize all paths consistently function normalizePath(p) { return path.normalize(p).toLowerCase(); } function expandHome(filepath) { if (filepath.startsWith('~/') || filepath === '~') { return path.join(os.homedir(), filepath.slice(1)); } return filepath; } // Security utilities export async function validatePath(requestedPath) { // Temporarily allow all paths by just returning the resolved path // TODO: Implement configurable path validation // Expand home directory if present const expandedPath = expandHome(requestedPath); // Convert to absolute path const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath); // Check if path exists try { const stats = await fs.stat(absolute); // If path exists, resolve any symlinks return await fs.realpath(absolute); } catch (error) { // return path if it's not exist. This will be used for folder creation and many other file operations return absolute; } /* Original implementation commented out for future reference const expandedPath = expandHome(requestedPath); const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath); const normalizedRequested = normalizePath(absolute); // Check if path is within allowed directories const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(normalizePath(dir))); if (!isAllowed) { throw new Error(`Access denied - path outside allowed directories: ${absolute}`); } // Handle symlinks by checking their real path try { const realPath = await fs.realpath(absolute); const normalizedReal = normalizePath(realPath); const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(normalizePath(dir))); if (!isRealPathAllowed) { throw new Error("Access denied - symlink target outside allowed directories"); } return realPath; } catch (error) { // For new files that don't exist yet, verify parent directory const parentDir = path.dirname(absolute); try { const realParentPath = await fs.realpath(parentDir); const normalizedParent = normalizePath(realParentPath); const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(normalizePath(dir))); if (!isParentAllowed) { throw new Error("Access denied - parent directory outside allowed directories"); } return absolute; } catch { throw new Error(`Parent directory does not exist: ${parentDir}`); } } */ } // File operation tools export async function readFile(filePath) { const validPath = await validatePath(filePath); return fs.readFile(validPath, "utf-8"); } export async function writeFile(filePath, content) { const validPath = await validatePath(filePath); await fs.writeFile(validPath, content, "utf-8"); } export async function readMultipleFiles(paths) { return Promise.all(paths.map(async (filePath) => { try { const validPath = await validatePath(filePath); const content = await fs.readFile(validPath, "utf-8"); return `${filePath}:\n${content}\n`; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return `${filePath}: Error - ${errorMessage}`; } })); } export async function createDirectory(dirPath) { const validPath = await validatePath(dirPath); await fs.mkdir(validPath, { recursive: true }); } export async function listDirectory(dirPath) { const validPath = await validatePath(dirPath); const entries = await fs.readdir(validPath, { withFileTypes: true }); return entries.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`); } export async function moveFile(sourcePath, destinationPath) { const validSourcePath = await validatePath(sourcePath); const validDestPath = await validatePath(destinationPath); await fs.rename(validSourcePath, validDestPath); } export async function searchFiles(rootPath, pattern) { const results = []; async function search(currentPath) { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); try { await validatePath(fullPath); if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { results.push(fullPath); } if (entry.isDirectory()) { await search(fullPath); } } catch (error) { continue; } } } // if path not exist, it will throw an error const validPath = await validatePath(rootPath); await search(validPath); return results; } export async function getFileInfo(filePath) { const validPath = await validatePath(filePath); const stats = await fs.stat(validPath); 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 function listAllowedDirectories() { return ["/ (All paths are currently allowed)"]; }