@modelcontextprotocol/server-filesystem
Version:
MCP server for filesystem access
151 lines (150 loc) • 6.6 kB
JavaScript
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);
}
}