@@ -35,7 +35,7 @@ function expandHome(filepath: string): string {
35
35
}
36
36
37
37
// Store allowed directories in normalized form
38
- const allowedDirectories = args . map ( dir =>
38
+ const allowedDirectories = args . map ( dir =>
39
39
normalizePath ( path . resolve ( expandHome ( dir ) ) )
40
40
) ;
41
41
@@ -59,7 +59,7 @@ async function validatePath(requestedPath: string): Promise<string> {
59
59
const absolute = path . isAbsolute ( expandedPath )
60
60
? path . resolve ( expandedPath )
61
61
: path . resolve ( process . cwd ( ) , expandedPath ) ;
62
-
62
+
63
63
const normalizedRequested = normalizePath ( absolute ) ;
64
64
65
65
// Check if path is within allowed directories
@@ -127,6 +127,10 @@ const ListDirectoryArgsSchema = z.object({
127
127
path : z . string ( ) ,
128
128
} ) ;
129
129
130
+ const DirectoryTreeArgsSchema = z . object ( {
131
+ path : z . string ( ) ,
132
+ } ) ;
133
+
130
134
const MoveFileArgsSchema = z . object ( {
131
135
source : z . string ( ) ,
132
136
destination : z . string ( ) ,
@@ -237,7 +241,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
237
241
// Ensure consistent line endings for diff
238
242
const normalizedOriginal = normalizeLineEndings ( originalContent ) ;
239
243
const normalizedNew = normalizeLineEndings ( newContent ) ;
240
-
244
+
241
245
return createTwoFilesPatch (
242
246
filepath ,
243
247
filepath ,
@@ -255,33 +259,33 @@ async function applyFileEdits(
255
259
) : Promise < string > {
256
260
// Read file content and normalize line endings
257
261
const content = normalizeLineEndings ( await fs . readFile ( filePath , 'utf-8' ) ) ;
258
-
262
+
259
263
// Apply edits sequentially
260
264
let modifiedContent = content ;
261
265
for ( const edit of edits ) {
262
266
const normalizedOld = normalizeLineEndings ( edit . oldText ) ;
263
267
const normalizedNew = normalizeLineEndings ( edit . newText ) ;
264
-
268
+
265
269
// If exact match exists, use it
266
270
if ( modifiedContent . includes ( normalizedOld ) ) {
267
271
modifiedContent = modifiedContent . replace ( normalizedOld , normalizedNew ) ;
268
272
continue ;
269
273
}
270
-
274
+
271
275
// Otherwise, try line-by-line matching with flexibility for whitespace
272
276
const oldLines = normalizedOld . split ( '\n' ) ;
273
277
const contentLines = modifiedContent . split ( '\n' ) ;
274
278
let matchFound = false ;
275
-
279
+
276
280
for ( let i = 0 ; i <= contentLines . length - oldLines . length ; i ++ ) {
277
281
const potentialMatch = contentLines . slice ( i , i + oldLines . length ) ;
278
-
282
+
279
283
// Compare lines with normalized whitespace
280
284
const isMatch = oldLines . every ( ( oldLine , j ) => {
281
285
const contentLine = potentialMatch [ j ] ;
282
286
return oldLine . trim ( ) === contentLine . trim ( ) ;
283
287
} ) ;
284
-
288
+
285
289
if ( isMatch ) {
286
290
// Preserve original indentation of first line
287
291
const originalIndent = contentLines [ i ] . match ( / ^ \s * / ) ?. [ 0 ] || '' ;
@@ -296,33 +300,33 @@ async function applyFileEdits(
296
300
}
297
301
return line ;
298
302
} ) ;
299
-
303
+
300
304
contentLines . splice ( i , oldLines . length , ...newLines ) ;
301
305
modifiedContent = contentLines . join ( '\n' ) ;
302
306
matchFound = true ;
303
307
break ;
304
308
}
305
309
}
306
-
310
+
307
311
if ( ! matchFound ) {
308
312
throw new Error ( `Could not find exact match for edit:\n${ edit . oldText } ` ) ;
309
313
}
310
314
}
311
-
315
+
312
316
// Create unified diff
313
317
const diff = createUnifiedDiff ( content , modifiedContent , filePath ) ;
314
-
318
+
315
319
// Format diff with appropriate number of backticks
316
320
let numBackticks = 3 ;
317
321
while ( diff . includes ( '`' . repeat ( numBackticks ) ) ) {
318
322
numBackticks ++ ;
319
323
}
320
324
const formattedDiff = `${ '`' . repeat ( numBackticks ) } diff\n${ diff } ${ '`' . repeat ( numBackticks ) } \n\n` ;
321
-
325
+
322
326
if ( ! dryRun ) {
323
327
await fs . writeFile ( filePath , modifiedContent , 'utf-8' ) ;
324
328
}
325
-
329
+
326
330
return formattedDiff ;
327
331
}
328
332
@@ -383,6 +387,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
383
387
"finding specific files within a directory. Only works within allowed directories." ,
384
388
inputSchema : zodToJsonSchema ( ListDirectoryArgsSchema ) as ToolInput ,
385
389
} ,
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
+ } ,
386
399
{
387
400
name : "move_file" ,
388
401
description :
@@ -413,7 +426,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
413
426
} ,
414
427
{
415
428
name : "list_allowed_directories" ,
416
- description :
429
+ description :
417
430
"Returns the list of directories that this server is allowed to access. " +
418
431
"Use this to understand which directories are available before trying to access files." ,
419
432
inputSchema : {
@@ -517,6 +530,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
517
530
} ;
518
531
}
519
532
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
+
520
576
case "move_file" : {
521
577
const parsed = MoveFileArgsSchema . safeParse ( args ) ;
522
578
if ( ! parsed . success ) {
0 commit comments