@modelcontextprotocol/server-filesystem
Version:
MCP server for filesystem access
62 lines (61 loc) • 3.36 kB
JavaScript
// How the filesystem server reports its allow-list to the operator (#4912),
// and how --confine-roots keeps a client's Roots inside it (#3133).
// The allowed directories in force can come from the command line or, on a
// 2025-era connection, from the client's Roots, and they change at runtime as
// the client's Roots do. index.ts logs them at startup and server.ts after
// every change, both through describeAllowedDirectories, so the message reads
// the same wherever it is printed and an operator can tell the effective
// access state and its source from stderr alone, without calling a tool.
// confineRootsToDirectories is a pure function of the two lists, so the
// policy is testable apart from the roots/list exchange that feeds it.
import { isPathWithinAllowedDirectories } from "./path-validation.js";
/**
* The one-message summary of the allowed directories in force, logged to
* stderr at startup and after every change, so an operator can read the
* effective access state and its source without calling a tool (#4912). Each
* directory is on a line of its own, since a path may contain a comma.
*/
export function describeAllowedDirectories(directories, source) {
if (directories.length === 0) {
return source.from === "roots"
? "Effective allowed directories: none. The client's Roots name no valid directory, so every path is refused until it exposes one again"
: "Effective allowed directories: none. No command-line directories and no valid client Roots, so every path is refused";
}
const origin = source.from === "command-line"
? "from the command line"
: source.confined
? "from the client's Roots, confined to the command-line directories"
: source.replacedCommandLine
? "from the client's Roots, which replaced the command-line directories"
: "from the client's Roots";
return [
`Effective allowed directories (${origin}):`,
...directories.map((dir) => ` ${dir}`),
].join("\n");
}
/**
* The client's Roots confined to the operator's command-line directories
* (`--confine-roots`, #3133): the overlap of the two lists, so a client's
* Roots can narrow the command-line directories but never widen them. A root
* inside a command-line directory is kept; a root that contains one is
* narrowed to that directory; a root that overlaps none is ignored, and
* returned in `ignored` so the caller can log it. Both lists hold resolved,
* normalized directories (each spelling of a symlinked one), and every request
* is still realpath'd and checked against the result, so a root's advertised
* spelling kept here cannot reach past its target.
*/
export function confineRootsToDirectories(rootDirectories, commandLineDirectories) {
const confined = new Set();
const ignored = [];
for (const root of rootDirectories) {
const inside = isPathWithinAllowedDirectories(root, commandLineDirectories);
const contained = commandLineDirectories.filter((dir) => isPathWithinAllowedDirectories(dir, [root]));
if (inside)
confined.add(root);
for (const dir of contained)
confined.add(dir);
if (!inside && contained.length === 0)
ignored.push(root);
}
return { confined: [...confined], ignored };
}