@@ -35,7 +35,7 @@ function expandHome(filepath: string): string {
3535}
3636
3737// Store allowed directories in normalized form
38- const allowedDirectories = args . map ( dir =>
38+ const allowedDirectories = args . map ( dir =>
3939 normalizePath ( path . resolve ( expandHome ( dir ) ) )
4040) ;
4141
@@ -59,7 +59,7 @@ async function validatePath(requestedPath: string): Promise<string> {
5959 const absolute = path . isAbsolute ( expandedPath )
6060 ? path . resolve ( expandedPath )
6161 : path . resolve ( process . cwd ( ) , expandedPath ) ;
62-
62+
6363 const normalizedRequested = normalizePath ( absolute ) ;
6464
6565 // Check if path is within allowed directories
@@ -127,6 +127,10 @@ const ListDirectoryArgsSchema = z.object({
127127 path : z . string ( ) ,
128128} ) ;
129129
130+ const DirectoryTreeArgsSchema = z . object ( {
131+ path : z . string ( ) ,
132+ } ) ;
133+
130134const MoveFileArgsSchema = z . object ( {
131135 source : z . string ( ) ,
132136 destination : z . string ( ) ,
@@ -237,7 +241,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
237241 // Ensure consistent line endings for diff
238242 const normalizedOriginal = normalizeLineEndings ( originalContent ) ;
239243 const normalizedNew = normalizeLineEndings ( newContent ) ;
240-
244+
241245 return createTwoFilesPatch (
242246 filepath ,
243247 filepath ,
@@ -255,33 +259,33 @@ async function applyFileEdits(
255259) : Promise < string > {
256260 // Read file content and normalize line endings
257261 const content = normalizeLineEndings ( await fs . readFile ( filePath , 'utf-8' ) ) ;
258-
262+
259263 // Apply edits sequentially
260264 let modifiedContent = content ;
261265 for ( const edit of edits ) {
262266 const normalizedOld = normalizeLineEndings ( edit . oldText ) ;
263267 const normalizedNew = normalizeLineEndings ( edit . newText ) ;
264-
268+
265269 // If exact match exists, use it
266270 if ( modifiedContent . includes ( normalizedOld ) ) {
267271 modifiedContent = modifiedContent . replace ( normalizedOld , normalizedNew ) ;
268272 continue ;
269273 }
270-
274+
271275 // Otherwise, try line-by-line matching with flexibility for whitespace
272276 const oldLines = normalizedOld . split ( '\n' ) ;
273277 const contentLines = modifiedContent . split ( '\n' ) ;
274278 let matchFound = false ;
275-
279+
276280 for ( let i = 0 ; i <= contentLines . length - oldLines . length ; i ++ ) {
277281 const potentialMatch = contentLines . slice ( i , i + oldLines . length ) ;
278-
282+
279283 // Compare lines with normalized whitespace
280284 const isMatch = oldLines . every ( ( oldLine , j ) => {
281285 const contentLine = potentialMatch [ j ] ;
282286 return oldLine . trim ( ) === contentLine . trim ( ) ;
283287 } ) ;
284-
288+
285289 if ( isMatch ) {
286290 // Preserve original indentation of first line
287291 const originalIndent = contentLines [ i ] . match ( / ^ \s * / ) ?. [ 0 ] || '' ;
@@ -296,33 +300,33 @@ async function applyFileEdits(
296300 }
297301 return line ;
298302 } ) ;
299-
303+
300304 contentLines . splice ( i , oldLines . length , ...newLines ) ;
301305 modifiedContent = contentLines . join ( '\n' ) ;
302306 matchFound = true ;
303307 break ;
304308 }
305309 }
306-
310+
307311 if ( ! matchFound ) {
308312 throw new Error ( `Could not find exact match for edit:\n${ edit . oldText } ` ) ;
309313 }
310314 }
311-
315+
312316 // Create unified diff
313317 const diff = createUnifiedDiff ( content , modifiedContent , filePath ) ;
314-
318+
315319 // Format diff with appropriate number of backticks
316320 let numBackticks = 3 ;
317321 while ( diff . includes ( '`' . repeat ( numBackticks ) ) ) {
318322 numBackticks ++ ;
319323 }
320324 const formattedDiff = `${ '`' . repeat ( numBackticks ) } diff\n${ diff } ${ '`' . repeat ( numBackticks ) } \n\n` ;
321-
325+
322326 if ( ! dryRun ) {
323327 await fs . writeFile ( filePath , modifiedContent , 'utf-8' ) ;
324328 }
325-
329+
326330 return formattedDiff ;
327331}
328332
@@ -383,6 +387,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
383387 "finding specific files within a directory. Only works within allowed directories." ,
384388 inputSchema : zodToJsonSchema ( ListDirectoryArgsSchema ) as ToolInput ,
385389 } ,
390+ {
391+ name : "directory_tree" ,
392+ description :
393+ "Get a recursive tree view of files and directories as a JSON structure. " +
394+ "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
395+ "Files have no children array, while directories always have a children array (which may be empty). " +
396+ "The output is formatted with 2-space indentation for readability. Only works within allowed directories." ,
397+ inputSchema : zodToJsonSchema ( DirectoryTreeArgsSchema ) as ToolInput ,
398+ } ,
386399 {
387400 name : "move_file" ,
388401 description :
@@ -413,7 +426,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
413426 } ,
414427 {
415428 name : "list_allowed_directories" ,
416- description :
429+ description :
417430 "Returns the list of directories that this server is allowed to access. " +
418431 "Use this to understand which directories are available before trying to access files." ,
419432 inputSchema : {
@@ -517,6 +530,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
517530 } ;
518531 }
519532
533+ case "directory_tree" : {
534+ const parsed = DirectoryTreeArgsSchema . safeParse ( args ) ;
535+ if ( ! parsed . success ) {
536+ throw new Error ( `Invalid arguments for directory_tree: ${ parsed . error } ` ) ;
537+ }
538+
539+ interface TreeEntry {
540+ name: string ;
541+ type: 'file' | 'directory' ;
542+ children ?: TreeEntry [ ] ;
543+ }
544+
545+ async function buildTree ( currentPath : string ) : Promise < TreeEntry [ ] > {
546+ const validPath = await validatePath ( currentPath ) ;
547+ const entries = await fs . readdir ( validPath , { withFileTypes : true } ) ;
548+ const result : TreeEntry [ ] = [ ] ;
549+
550+ for ( const entry of entries ) {
551+ const entryData : TreeEntry = {
552+ name : entry . name ,
553+ type : entry . isDirectory ( ) ? 'directory' : 'file'
554+ } ;
555+
556+ if ( entry . isDirectory ( ) ) {
557+ const subPath = path . join ( currentPath , entry . name ) ;
558+ entryData . children = await buildTree ( subPath ) ;
559+ }
560+
561+ result . push ( entryData ) ;
562+ }
563+
564+ return result ;
565+ }
566+
567+ const treeData = await buildTree ( parsed . data . path ) ;
568+ return {
569+ content : [ {
570+ type : "text" ,
571+ text : JSON . stringify ( treeData , null , 2 )
572+ } ] ,
573+ } ;
574+ }
575+
520576 case "move_file" : {
521577 const parsed = MoveFileArgsSchema . safeParse ( args ) ;
522578 if ( ! parsed . success ) {
0 commit comments