@modelcontextprotocol/server-filesystem
Version:
MCP server for filesystem access
84 lines (83 loc) • 3.49 kB
JavaScript
import { promises as fs } from "fs";
import path from "path";
import { normalizePathFormat, realpathWithRetry, expandHome, } from "./path-utils.js";
import { fileURLToPath } from "url";
/**
* Converts a root URI to its normalized absolute path and its realpath.
* Both are kept, as resolveAllowedDirectories in index.ts keeps both forms of
* a command-line directory, so a request may use either spelling (#5183).
* validatePath still realpaths every request, so access stays bounded by the
* resolved directory.
* @param rootUri - File URI (file://...) or plain directory path
* @returns Promise resolving to both forms, or null if invalid
*/
async function parseRootUri(rootUri) {
try {
const rawPath = rootUri.startsWith("file://")
? fileURLToPath(rootUri)
: rootUri;
const expandedPath = expandHome(rawPath);
const absolutePath = path.resolve(expandedPath);
const resolvedPath = await realpathWithRetry(absolutePath);
return {
// normalizePathFormat, not normalizePath, for both: trimming whitespace
// or quotes from the client's spelling or from the realpath would name
// a different directory, and allow it (#5183, #5320).
advertised: normalizePathFormat(absolutePath),
resolved: normalizePathFormat(resolvedPath),
};
}
catch {
return null; // Path doesn't exist or other error
}
}
/**
* Formats error message for directory validation failures.
* @param dir - Directory path that failed validation
* @param error - Error that occurred during validation
* @param reason - Specific reason for failure
* @returns Formatted error message
*/
function formatDirectoryError(dir, error, reason) {
if (reason) {
return `Skipping ${reason}: ${dir}`;
}
const message = error instanceof Error ? error.message : String(error);
return `Skipping invalid directory: ${dir} due to error: ${message}`;
}
/**
* Resolves requested root directories from MCP root specifications.
*
* Converts root URI specifications (file:// URIs or plain paths) into normalized
* directory paths, validating that each path exists and is a directory.
* Each root contributes its advertised form and, when it differs, its resolved
* (realpath) form, deduplicated across roots.
*
* @param requestedRoots - Array of root specifications with URI and optional name
* @returns Promise resolving to array of validated directory paths
*/
export async function getValidRootDirectories(requestedRoots) {
const validatedDirectories = new Set();
for (const requestedRoot of requestedRoots) {
const rootPaths = await parseRootUri(requestedRoot.uri);
if (!rootPaths) {
console.error(formatDirectoryError(requestedRoot.uri, undefined, "invalid path or inaccessible"));
continue;
}
const { advertised, resolved: resolvedPath } = rootPaths;
try {
const stats = await fs.stat(resolvedPath);
if (stats.isDirectory()) {
validatedDirectories.add(advertised);
validatedDirectories.add(resolvedPath);
}
else {
console.error(formatDirectoryError(resolvedPath, undefined, "non-directory root"));
}
}
catch (error) {
console.error(formatDirectoryError(resolvedPath, error));
}
}
return [...validatedDirectories];
}