Skip to content
Closed
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
14 changes: 14 additions & 0 deletions src/filesystem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio

The server uses a flexible directory access control system. Directories can be specified via command-line arguments or dynamically via [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots).

### Hidden Files and Directories

By default, the filesystem server **ignores dot-prefixed files and directories** (like `.git`, `.env`, `.terraform`, etc.) to:
- Reduce token usage (especially from large directories like `.git`)
- Enhance security by avoiding exposure of potentially sensitive hidden files
- Follow the convention that dot-prefixed items are typically "hidden" for good reason

To include hidden files and directories, set the environment variable:
```bash
export MCP_FILESYSTEM_INCLUDE_HIDDEN=true
```

This affects the following operations: `list_directory`, `list_directory_with_sizes`, `directory_tree`, and `search_files`.

### Method 1: Command-line Arguments
Specify Allowed directories when starting the server:
```bash
Expand Down
84 changes: 84 additions & 0 deletions src/filesystem/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
searchFilesWithValidation,
// File editing functions
applyFileEdits,
// Hidden files filtering functions
shouldIncludeHidden,
shouldFilterHiddenEntry,
tailFile,
headFile
} from '../lib.js';
Expand Down Expand Up @@ -699,3 +702,84 @@ describe('Lib Functions', () => {
});
});
});

describe('Hidden Files Filtering', () => {
describe('shouldIncludeHidden', () => {
const originalEnv = process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN;

afterEach(() => {
// Restore original environment variable
if (originalEnv === undefined) {
delete process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN;
} else {
process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN = originalEnv;
}
});

it('should return false by default (when env var not set)', () => {
delete process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN;
expect(shouldIncludeHidden()).toBe(false);
});

it('should return false when env var is set to "false"', () => {
process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN = 'false';
expect(shouldIncludeHidden()).toBe(false);
});

it('should return true when env var is set to "true"', () => {
process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN = 'true';
expect(shouldIncludeHidden()).toBe(true);
});

it('should return false for any other value', () => {
process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN = 'yes';
expect(shouldIncludeHidden()).toBe(false);
});
});

describe('shouldFilterHiddenEntry', () => {
const originalEnv = process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN;

afterEach(() => {
// Restore original environment variable
if (originalEnv === undefined) {
delete process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN;
} else {
process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN = originalEnv;
}
});

it('should filter dot-prefixed names by default', () => {
delete process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN;
expect(shouldFilterHiddenEntry('.git')).toBe(true);
expect(shouldFilterHiddenEntry('.env')).toBe(true);
expect(shouldFilterHiddenEntry('.terraform')).toBe(true);
expect(shouldFilterHiddenEntry('.vscode')).toBe(true);
});

it('should not filter regular names by default', () => {
delete process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN;
expect(shouldFilterHiddenEntry('README.md')).toBe(false);
expect(shouldFilterHiddenEntry('package.json')).toBe(false);
expect(shouldFilterHiddenEntry('src')).toBe(false);
expect(shouldFilterHiddenEntry('test.txt')).toBe(false);
});

it('should not filter any names when hidden files are included', () => {
process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN = 'true';
expect(shouldFilterHiddenEntry('.git')).toBe(false);
expect(shouldFilterHiddenEntry('.env')).toBe(false);
expect(shouldFilterHiddenEntry('README.md')).toBe(false);
expect(shouldFilterHiddenEntry('package.json')).toBe(false);
});

it('should handle edge cases correctly', () => {
delete process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN;
expect(shouldFilterHiddenEntry('')).toBe(false); // empty string
expect(shouldFilterHiddenEntry('.')).toBe(true); // single dot
expect(shouldFilterHiddenEntry('..')).toBe(true); // double dot
expect(shouldFilterHiddenEntry('name.')).toBe(false); // dot at end
expect(shouldFilterHiddenEntry('name.with.dots')).toBe(false); // dots in middle
});
});
});
47 changes: 27 additions & 20 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
tailFile,
headFile,
setAllowedDirectories,
shouldFilterHiddenEntry,
} from './lib.js';

// Command line argument parsing
Expand Down Expand Up @@ -449,6 +450,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const validPath = await validatePath(parsed.data.path);
const entries = await fs.readdir(validPath, { withFileTypes: true });
const formatted = entries
.filter((entry) => !shouldFilterHiddenEntry(entry.name))
.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
.join("\n");
return {
Expand All @@ -464,27 +466,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const validPath = await validatePath(parsed.data.path);
const entries = await fs.readdir(validPath, { withFileTypes: true });

// Get detailed information for each entry
// Get detailed information for each entry (excluding hidden entries)
const detailedEntries = await Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(validPath, entry.name);
try {
const stats = await fs.stat(entryPath);
return {
name: entry.name,
isDirectory: entry.isDirectory(),
size: stats.size,
mtime: stats.mtime
};
} catch (error) {
return {
name: entry.name,
isDirectory: entry.isDirectory(),
size: 0,
mtime: new Date(0)
};
}
})
entries
.filter((entry) => !shouldFilterHiddenEntry(entry.name))
.map(async (entry) => {
const entryPath = path.join(validPath, entry.name);
try {
const stats = await fs.stat(entryPath);
return {
name: entry.name,
isDirectory: entry.isDirectory(),
size: stats.size,
mtime: stats.mtime
};
} catch (error) {
return {
name: entry.name,
isDirectory: entry.isDirectory(),
size: 0,
mtime: new Date(0)
};
}
})
);

// Sort entries based on sortBy parameter
Expand Down Expand Up @@ -541,6 +545,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const result: TreeEntry[] = [];

for (const entry of entries) {
// Skip hidden files/directories unless explicitly enabled
if (shouldFilterHiddenEntry(entry.name)) continue;

const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));
const shouldExclude = excludePatterns.some(pattern => {
if (pattern.includes('*')) {
Expand Down
14 changes: 14 additions & 0 deletions src/filesystem/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ export interface SearchResult {
isDirectory: boolean;
}

// Check if hidden files/directories should be included
// Environment variable MCP_FILESYSTEM_INCLUDE_HIDDEN controls this (default: false)
export function shouldIncludeHidden(): boolean {
return process.env.MCP_FILESYSTEM_INCLUDE_HIDDEN === 'true';
}

// Check if a file/directory name should be filtered out (dot-prefixed items)
export function shouldFilterHiddenEntry(name: string): boolean {
return !shouldIncludeHidden() && name.startsWith('.');
}

// Pure Utility Functions
export function formatSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
Expand Down Expand Up @@ -361,6 +372,9 @@ export async function searchFilesWithValidation(
const entries = await fs.readdir(currentPath, { withFileTypes: true });

for (const entry of entries) {
// Skip hidden files/directories unless explicitly enabled
if (shouldFilterHiddenEntry(entry.name)) continue;

const fullPath = path.join(currentPath, entry.name);

try {
Expand Down