Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/filesystem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ The server's directory access control follows this flow:



**Security Note**: You can prevent write operations to sensitive files (such as `.env`, `.env.*`, or any custom pattern) by using the `--ignore-write` command-line argument. See below for usage.

## Usage

### Command-line Arguments

```
mcp-server-filesystem <allowed-directory> [additional-directories...] [--ignore-write <pattern1> <pattern2> ...]
```

- `<allowed-directory>`: One or more directories the server is allowed to access.
- `--ignore-write <pattern1> <pattern2> ...`: (Optional) List of filenames or glob patterns to block from write operations. Example: `--ignore-write .env .env.* *.secret`

If a file matches any of the ignore patterns, write operations to that file will be blocked, even if it is inside an allowed directory.

## API

### Resources
Expand Down
87 changes: 62 additions & 25 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,35 @@ import { getValidRootDirectories } from './roots-utils.js';

// Command line argument parsing
const args = process.argv.slice(2);

if (args.length === 0) {
console.error("Usage: mcp-server-filesystem [allowed-directory] [additional-directories...]");
console.error("Note: Allowed directories can be provided via:");
console.error(" 1. Command-line arguments (shown above)");
console.error(" 2. MCP roots protocol (if client supports it)");
console.error("At least one directory must be provided by EITHER method for the server to operate.");

}

// Support: mcp-server-filesystem <allowed-directory> [additional-directories...] [--ignore-write <pattern1> <pattern2> ...]
let allowedDirs: string[] = [];
let ignoreWritePatterns: string[] = [];

const ignoreFlagIndex = args.indexOf('--ignore-write');
if (ignoreFlagIndex !== -1) {
allowedDirs = args.slice(0, ignoreFlagIndex);
ignoreWritePatterns = args.slice(ignoreFlagIndex + 1);
} else {
allowedDirs = args;
}

if (allowedDirs.length === 0) {
console.error("Usage: mcp-server-filesystem <allowed-directory> [additional-directories...] [--ignore-write <pattern1> <pattern2> ...]");
process.exit(1);
}



// Normalize all paths consistently
function normalizePath(p: string): string {
return path.normalize(p);
Expand Down Expand Up @@ -58,6 +79,7 @@ let allowedDirectories = await Promise.all(
return normalizePath(absolute);
}
})

);

// Validate that all directories exist and are accessible
Expand Down Expand Up @@ -728,35 +750,50 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
const validPath = await validatePath(parsed.data.path);

try {
// Security: 'wx' flag ensures exclusive creation - fails if file/symlink exists,
// preventing writes through pre-existing symlinks
await fs.writeFile(validPath, parsed.data.content, { encoding: "utf-8", flag: 'wx' });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
// Security: Use atomic rename to prevent race conditions where symlinks
// could be created between validation and write. Rename operations
// replace the target file atomically and don't follow symlinks.
const tempPath = `${validPath}.${randomBytes(16).toString('hex')}.tmp`;
try {
await fs.writeFile(tempPath, parsed.data.content, 'utf-8');
await fs.rename(tempPath, validPath);
} catch (renameError) {
// Prevent writing to files matching ignoreWritePatterns
const baseName = path.basename(validPath);
const shouldIgnore = ignoreWritePatterns.some(pattern => {
// Simple glob-like match: support *.env, .env, .env.*, etc.
if (pattern.includes('*')) {
// Convert pattern to regex
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
return regex.test(baseName);
}
return baseName === pattern;
}) ;
if (shouldIgnore) {
throw new Error(`Write operation to file '${baseName}' is not allowed by server policy (matched ignore pattern).`);
}

try {
// Security: 'wx' flag ensures exclusive creation - fails if file/symlink exists,
// preventing writes through pre-existing symlinks
await fs.writeFile(validPath, parsed.data.content, { encoding: "utf-8", flag: 'wx' });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
// Security: Use atomic rename to prevent race conditions where symlinks
// could be created between validation and write. Rename operations
// replace the target file atomically and don't follow symlinks.
const tempPath = `${validPath}.${randomBytes(16).toString('hex')}.tmp`;
try {
await fs.unlink(tempPath);
} catch {}
throw renameError;
await fs.writeFile(tempPath, parsed.data.content, 'utf-8');
await fs.rename(tempPath, validPath);
} catch (renameError) {
try {
await fs.unlink(tempPath);
} catch {}
throw renameError;
}
} else {
throw error;
}
} else {
throw error;
}
}

return {
content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }],
};
}

return {
content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }],
};
}

case "edit_file": {
const parsed = EditFileArgsSchema.safeParse(args);
if (!parsed.success) {
Expand Down