mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-22 05:45:15 +02:00
feat(filesystem): ignore dot directories by default to reduce token usage
- Add environment variable MCP_FILESYSTEM_INCLUDE_HIDDEN (default: false) - Filter dot-prefixed files/directories in list_directory, directory_tree, search_files - Reduces token usage from large directories like .git, .terraform, etc. - Enhances security by avoiding exposure of potentially sensitive hidden files - Includes comprehensive tests for the new filtering functionality - Update documentation to explain the new behavior and environment variable Fixes #2219 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Ola Hungerford <olaservo@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -18,6 +18,9 @@ import {
|
||||
searchFilesWithValidation,
|
||||
// File editing functions
|
||||
applyFileEdits,
|
||||
// Hidden files filtering functions
|
||||
shouldIncludeHidden,
|
||||
shouldFilterHiddenEntry,
|
||||
tailFile,
|
||||
headFile
|
||||
} from '../lib.js';
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
tailFile,
|
||||
headFile,
|
||||
setAllowedDirectories,
|
||||
shouldFilterHiddenEntry,
|
||||
} from './lib.js';
|
||||
|
||||
// Command line argument parsing
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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('*')) {
|
||||
|
||||
@@ -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'];
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user