diff --git a/src/filesystem/README.md b/src/filesystem/README.md index ac63f39a5f..746ec4fdac 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -123,6 +123,15 @@ The server's directory access control follows this flow: - List directory contents with [FILE] or [DIR] prefixes - Input: `path` (string) +- **directory_tree** + - Get a recursive tree view of files and directories as a JSON structure + - Each entry includes 'name', 'type' (file/directory), and 'children' for directories + - Files have no children array, while directories always have a children array + - Inputs: + - `path` (string): Starting directory + - `excludePatterns` (string[]): Exclude any patterns. Glob formats are supported. + - Output is formatted with 2-space indentation for readability + - **move_file** - Move or rename files and directories - Inputs: diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 6723f43600..982184ecdc 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -162,6 +162,7 @@ const ListDirectoryWithSizesArgsSchema = z.object({ const DirectoryTreeArgsSchema = z.object({ path: z.string(), + excludePatterns: z.array(z.string()).optional().default([]) }); const MoveFileArgsSchema = z.object({ @@ -581,6 +582,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { "Get a recursive tree view of files and directories as a JSON structure. " + "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + "Files have no children array, while directories always have a children array (which may be empty). " + + "Supports excluding files and directories using glob patterns. " + "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput, }, @@ -874,12 +876,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { children?: TreeEntry[]; } - async function buildTree(currentPath: string): Promise { + async function buildTree(rootPath: string, currentPath: string, excludePatterns: string[] = []): Promise { const validPath = await validatePath(currentPath); const entries = await fs.readdir(validPath, {withFileTypes: true}); const result: TreeEntry[] = []; for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + + // Check if path matches any exclude pattern + const relativePath = path.relative(rootPath, fullPath); + const shouldExclude = excludePatterns.some(pattern => { + const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`; + return minimatch(relativePath, globPattern, { dot: true }); + }); + + if (shouldExclude) { + continue; + } + const entryData: TreeEntry = { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file' @@ -887,7 +902,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(rootPath, subPath, excludePatterns); } result.push(entryData); @@ -896,7 +911,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return result; } - const treeData = await buildTree(parsed.data.path); + const validRootPath = await validatePath(parsed.data.path); + const treeData = await buildTree(validRootPath, validRootPath, parsed.data.excludePatterns); return { content: [{ type: "text",