feat(directory_tree): add excludePatterns support & documentation (#623)

- Update documentation with directory_tree declaration
- Add excludePatterns parameter to DirectoryTreeArgsSchema
- Implement pattern exclusion in buildTree function using minimatch
- Pass excludePatterns through recursive calls
- Support both simple and glob patterns for exclusion
- Maintain consistent behavior with search_files implementation

* Add tests and fix implementation

---------

Co-authored-by: Ola Hungerford <olahungerford@gmail.com>
Co-authored-by: Adam Jones <adamj+git@anthropic.com>
Co-authored-by: Adam Jones <adamj@anthropic.com>
This commit is contained in:
Enrico Ballardini
2025-08-23 08:19:01 +02:00
committed by GitHub
parent 46d0b1f926
commit d381cf1ffd
3 changed files with 180 additions and 3 deletions

View File

@@ -14,6 +14,7 @@ import { createReadStream } from "fs";
import path from "path";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { minimatch } from "minimatch";
import { normalizePath, expandHome } from './path-utils.js';
import { getValidRootDirectories } from './roots-utils.js';
import {
@@ -121,6 +122,7 @@ const ListDirectoryWithSizesArgsSchema = z.object({
const DirectoryTreeArgsSchema = z.object({
path: z.string(),
excludePatterns: z.array(z.string()).optional().default([])
});
const MoveFileArgsSchema = z.object({
@@ -528,13 +530,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
type: 'file' | 'directory';
children?: TreeEntry[];
}
const rootPath = parsed.data.path;
async function buildTree(currentPath: string): Promise<TreeEntry[]> {
async function buildTree(currentPath: string, excludePatterns: string[] = []): Promise<TreeEntry[]> {
const validPath = await validatePath(currentPath);
const entries = await fs.readdir(validPath, {withFileTypes: true});
const result: TreeEntry[] = [];
for (const entry of entries) {
const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));
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;
const entryData: TreeEntry = {
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file'
@@ -542,7 +559,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (entry.isDirectory()) {
const subPath = path.join(currentPath, entry.name);
entryData.children = await buildTree(subPath);
entryData.children = await buildTree(subPath, excludePatterns);
}
result.push(entryData);
@@ -551,7 +568,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
return result;
}
const treeData = await buildTree(parsed.data.path);
const treeData = await buildTree(rootPath, parsed.data.excludePatterns);
return {
content: [{
type: "text",