UNPKG

@modelcontextprotocol/server-filesystem

Version:
151 lines (150 loc) • 6.6 kB
import { promises as fs } from "fs"; import path from "path"; import os from "os"; import { setTimeout as delay } from "timers/promises"; /** * Converts WSL or Unix-style Windows paths to Windows format * @param p The path to convert * @returns Converted Windows path */ export function convertToWindowsPath(p) { // Handle WSL paths (/mnt/c/...) // NEVER convert WSL paths - they are valid Linux paths that work with Node.js fs operations in WSL // Converting them to Windows format (C:\...) breaks fs operations inside WSL if (p.startsWith("/mnt/")) { return p; // Leave WSL paths unchanged } // Handle Unix-style Windows paths (/c/...) // Only convert when running on Windows if (p.match(/^\/[a-zA-Z]\//) && process.platform === "win32") { const driveLetter = p.charAt(1).toUpperCase(); const pathPart = p.slice(2).replace(/\//g, "\\"); return `${driveLetter}:${pathPart}`; } // Handle standard Windows paths, ensuring backslashes if (p.match(/^[a-zA-Z]:/)) { return p.replace(/\//g, "\\"); } // Leave non-Windows paths unchanged return p; } /** * Normalizes path by standardizing format while preserving OS-specific behavior * @param p The path to normalize * @returns Normalized path */ export function normalizePath(p) { // Remove any surrounding quotes and whitespace return normalizePathFormat(p.trim().replace(/^["']|["']$/g, "")); } /** * Normalizes a path's format exactly as normalizePath does, but without * stripping surrounding quotes or whitespace. Those are legal filename * characters, so stripping them names a different file: a path that a file * operation will use must be checked against the allow-list in this form. * @param p The path to normalize * @returns Normalized path */ export function normalizePathFormat(p) { // Check if this is a Unix path that should not be converted // WSL paths (/mnt/) should ALWAYS be preserved as they work correctly in WSL with Node.js fs // Regular Unix paths should also be preserved const isUnixPath = p.startsWith("/") && // Always preserve WSL paths (/mnt/c/, /mnt/d/, etc.) (p.match(/^\/mnt\/[a-z]\//i) || // On non-Windows platforms, treat all absolute paths as Unix paths process.platform !== "win32" || // On Windows, preserve Unix paths that aren't Unix-style Windows paths (/c/, /d/, etc.) (process.platform === "win32" && !p.match(/^\/[a-zA-Z]\//))); if (isUnixPath) { // For Unix paths, just normalize without converting to Windows format // Replace double slashes with single slashes and remove trailing slashes return p.replace(/\/+/g, "/").replace(/(?<!^)\/$/, ""); } // Convert Unix-style Windows paths (/c/, /d/) to Windows format if on Windows // This function will now leave /mnt/ paths unchanged p = convertToWindowsPath(p); // Handle double backslashes, preserving leading UNC \\ if (p.startsWith("\\\\")) { // For UNC paths, first normalize any excessive leading backslashes to exactly \\ // Then normalize double backslashes in the rest of the path let uncPath = p; // Replace multiple leading backslashes with exactly two uncPath = uncPath.replace(/^\\{2,}/, "\\\\"); // Now normalize any remaining double backslashes in the rest of the path const restOfPath = uncPath.substring(2).replace(/\\\\/g, "\\"); p = "\\\\" + restOfPath; } else { // For non-UNC paths, normalize all double backslashes p = p.replace(/\\\\/g, "\\"); } // On Windows, if we have a bare drive letter (e.g. "C:"), append a separator // so path.normalize doesn't return "C:." which can break path validation. if (process.platform === "win32" && /^[a-zA-Z]:$/.test(p)) { p = p + path.sep; } // Use Node's path normalization, which handles . and .. segments let normalized = path.normalize(p); // Fix UNC paths after normalization (path.normalize can remove a leading backslash) if (p.startsWith("\\\\") && !normalized.startsWith("\\\\")) { normalized = "\\" + normalized; } // Handle Windows paths: convert slashes and ensure drive letter is capitalized if (normalized.match(/^[a-zA-Z]:/)) { let result = normalized.replace(/\//g, "\\"); // Capitalize drive letter if present if (/^[a-z]:/.test(result)) { result = result.charAt(0).toUpperCase() + result.slice(1); } return result; } // On Windows, convert forward slashes to backslashes for relative paths // On Linux/Unix, preserve forward slashes if (process.platform === "win32") { return normalized.replace(/\//g, "\\"); } // On non-Windows platforms, keep the normalized path as-is return normalized; } /** * Expands home directory tildes in paths: a bare `~`, `~/…` and, on Windows * only, `~\…` (#5180). On POSIX a backslash is an ordinary filename * character, so `~\notes` there names a file and is left as it is. * @param filepath The path to expand * @returns Expanded path */ export function expandHome(filepath) { if (filepath === "~" || filepath.startsWith("~/") || (process.platform === "win32" && filepath.startsWith("~\\"))) { return path.join(os.homedir(), filepath.slice(1)); } return filepath; } /** How long realpathWithRetry waits before its one retry. */ export const REALPATH_RETRY_DELAY_MS = 100; /** * Resolves a path with fs.promises.realpath, retrying once after a short * delay if the first call fails. * * For the directories resolved once and kept (the command-line directories * at startup, and each client Root on a roots update), where a failure lasts * for the life of the process or the roots list. On Windows the first native * realpath right after `subst` maps a drive can throw before the mapping is * visible, and succeed a moment later (#5185). The retry goes through the * same fs.promises.realpath (the native implementation) that validatePath * resolves requests with, so both sides still agree on the spelling. * @param p The path to resolve * @param retryDelayMs How long to wait before the retry * @returns The resolved path; rejects with the second call's error */ export async function realpathWithRetry(p, retryDelayMs = REALPATH_RETRY_DELAY_MS) { try { return await fs.realpath(p); } catch { await delay(retryDelayMs); return await fs.realpath(p); } }