Repository navigation
filesystem: search_files does not expand non-wildcard excludePatterns like directory_tree does #5216
Copy link
Copy link
Open
Description
Summary
While auditing src/filesystem on v2.0.0 (origin/main) following the #4857 spec refactor, I found a behavioral mismatch in how excludePatterns is evaluated between directory_tree (src/filesystem/server.ts) and search_files (src/filesystem/lib.ts).
When an LLM or user passes a bare directory or file name in excludePatterns (for example, ["node_modules", ".git"]), directory_tree excludes both the directory and its entire subtree, whereas search_files still traverses into the excluded directory and returns all files inside it.
Server & Environment
- Server:
filesystem(@modelcontextprotocol/server-filesystemv2.0.0/origin/main) - Files:
src/filesystem/server.ts(buildTree) vs.src/filesystem/lib.ts(searchFilesWithValidation) - Protocol era / transport: Both
2026-07-28and2025-eraoverstdio/ in-memory client transport
Reproduction / Root Cause in v2/main
- In
directory_tree(src/filesystem/server.ts, lines 515–529),buildTreechecks whether an exclusion pattern contains*. If it does not, it automatically expands the pattern to match nested paths and subtrees (**/${pattern}and**/${pattern}/**), and skips recursing into the excluded directory:
const shouldExclude = excludePatterns.some((pattern) => {
if (pattern.includes("*")) {
return minimatch(relativePath, pattern, { dot: true });
}
// For files: match exact name or as part of path
// For directories: match as directory path
return (
minimatch(relativePath, pattern, { dot: true }) ||
minimatch(relativePath, `**/${pattern}`, { dot: true }) ||
minimatch(relativePath, `**/${pattern}/**`, { dot: true })
);
});
if (shouldExclude) continue;- In
search_files(src/filesystem/lib.ts, lines 397–413),searchFilesWithValidationevaluatesminimatch(relativePath, excludePattern, { dot: true })directly without the non-wildcard expansion:
const relativePath = path.relative(rootPath, fullPath);
const shouldExclude = excludePatterns.some((excludePattern) =>
minimatch(relativePath, excludePattern, { dot: true }),
);
if (shouldExclude) continue;What happens when a caller passes excludePatterns: ["node_modules"] or ["sub"]:
- At
node_modules(top-level):relativePathis"node_modules".minimatch("node_modules", "node_modules")istrue, soif (shouldExclude) continueskips recursing into top-levelnode_modules! - At
packages/app/node_modules(nested directory):relativePathis"packages/app/node_modules".minimatch("packages/app/node_modules", "node_modules")isfalse!search_filesenterspackages/app/node_modulesand traverses every file inside it. - When
excludePatterns: ["node_modules/*"]or["**/node_modules"](without trailing/**) is passed: Even if a pattern matches files inside a directory,search_filesstill wastes I/O recursing unless the directory entry itself matchesshouldExclude.
Expected vs. Actual Behavior
- Expected: Passing
excludePatterns: ["node_modules", ".git"](or a bare filename/directory name without*) tosearch_filesshould behave identically todirectory_tree: any nested directory or file matchingpattern,**/${pattern}, or**/${pattern}/**is excluded and its directory subtree is not traversed. - Actual:
directory_treeexcludes nestedpackages/app/node_modules, whilesearch_filestraverses intopackages/app/node_modulesand returns matches from inside it unless the caller explicitly writes**/node_modules/**.
Prompt to Reproduce Fix in the Agentic Workflow
Per CONTRIBUTING.md, here is the exact prompt to fix and test this in the factory workflow:
Unify `excludePatterns` matching between `directory_tree` (`src/filesystem/server.ts`) and `search_files` (`src/filesystem/lib.ts`):
1. Extract a shared helper (or update `searchFilesWithValidation` in `src/filesystem/lib.ts` to match `buildTree` in `src/filesystem/server.ts`) so that when an entry in `excludePatterns` does not contain `*`, it matches `minimatch(relativePath, pattern, { dot: true }) || minimatch(relativePath, `**/${pattern}`, { dot: true }) || minimatch(relativePath, `**/${pattern}/**`, { dot: true })`.
2. Add a protocol-level test in `src/filesystem/__tests__/server-tools.test.ts` (and unit test in `src/filesystem/__tests__/lib.test.ts`) verifying that `search_files` with a bare name in `excludePatterns` (e.g. `excludePatterns: ["ignored_dir"]`) excludes both top-level `ignored_dir/file.txt` and nested `nested/ignored_dir/file.txt`, matching `directory_tree`'s behavior.
3. Run `npm run format` and `npm run local:gate`.Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity