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
9 changes: 9 additions & 0 deletions src/filesystem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 19 additions & 3 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -874,20 +876,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
children?: TreeEntry[];
}

async function buildTree(currentPath: string): Promise<TreeEntry[]> {
async function buildTree(rootPath: string, 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 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'
};

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);
Expand All @@ -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",
Expand Down