@@ -97,6 +97,8 @@ async function validatePath(requestedPath: string): Promise<string> {
9797// Schema definitions
9898const ReadFileArgsSchema = z . object ( {
9999 path : z . string ( ) ,
100+ tail : z . number ( ) . optional ( ) . describe ( 'If provided, returns only the last N lines of the file' ) ,
101+ head : z . number ( ) . optional ( ) . describe ( 'If provided, returns only the first N lines of the file' )
100102} ) ;
101103
102104const ReadMultipleFilesArgsSchema = z . object ( {
@@ -127,6 +129,11 @@ const ListDirectoryArgsSchema = z.object({
127129 path : z . string ( ) ,
128130} ) ;
129131
132+ const ListDirectoryWithSizesArgsSchema = z . object ( {
133+ path : z . string ( ) ,
134+ sortBy : z . enum ( [ 'name' , 'size' ] ) . optional ( ) . default ( 'name' ) . describe ( 'Sort entries by name or size' ) ,
135+ } ) ;
136+
130137const DirectoryTreeArgsSchema = z . object ( {
131138 path : z . string ( ) ,
132139} ) ;
@@ -330,6 +337,107 @@ async function applyFileEdits(
330337 return formattedDiff ;
331338}
332339
340+ // Helper functions
341+ function formatSize ( bytes : number ) : string {
342+ const units = [ 'B' , 'KB' , 'MB' , 'GB' , 'TB' ] ;
343+ if ( bytes === 0 ) return '0 B' ;
344+
345+ const i = Math . floor ( Math . log ( bytes ) / Math . log ( 1024 ) ) ;
346+ if ( i === 0 ) return `${ bytes } ${ units [ i ] } ` ;
347+
348+ return `${ ( bytes / Math . pow ( 1024 , i ) ) . toFixed ( 2 ) } ${ units [ i ] } ` ;
349+ }
350+
351+ // Memory-efficient implementation to get the last N lines of a file
352+ async function tailFile ( filePath : string , numLines : number ) : Promise < string > {
353+ const CHUNK_SIZE = 1024 ; // Read 1KB at a time
354+ const stats = await fs . stat ( filePath ) ;
355+ const fileSize = stats . size ;
356+
357+ if ( fileSize === 0 ) return '' ;
358+
359+ // Open file for reading
360+ const fileHandle = await fs . open ( filePath , 'r' ) ;
361+ try {
362+ const lines : string [ ] = [ ] ;
363+ let position = fileSize ;
364+ let chunk = Buffer . alloc ( CHUNK_SIZE ) ;
365+ let linesFound = 0 ;
366+ let remainingText = '' ;
367+
368+ // Read chunks from the end of the file until we have enough lines
369+ while ( position > 0 && linesFound < numLines ) {
370+ const size = Math . min ( CHUNK_SIZE , position ) ;
371+ position -= size ;
372+
373+ const { bytesRead } = await fileHandle . read ( chunk , 0 , size , position ) ;
374+ if ( ! bytesRead ) break ;
375+
376+ // Get the chunk as a string and prepend any remaining text from previous iteration
377+ const readData = chunk . slice ( 0 , bytesRead ) . toString ( 'utf-8' ) ;
378+ const chunkText = readData + remainingText ;
379+
380+ // Split by newlines and count
381+ const chunkLines = normalizeLineEndings ( chunkText ) . split ( '\n' ) ;
382+
383+ // If this isn't the end of the file, the first line is likely incomplete
384+ // Save it to prepend to the next chunk
385+ if ( position > 0 ) {
386+ remainingText = chunkLines [ 0 ] ;
387+ chunkLines . shift ( ) ; // Remove the first (incomplete) line
388+ }
389+
390+ // Add lines to our result (up to the number we need)
391+ for ( let i = chunkLines . length - 1 ; i >= 0 && linesFound < numLines ; i -- ) {
392+ lines . unshift ( chunkLines [ i ] ) ;
393+ linesFound ++ ;
394+ }
395+ }
396+
397+ return lines . join ( '\n' ) ;
398+ } finally {
399+ await fileHandle . close ( ) ;
400+ }
401+ }
402+
403+ // New function to get the first N lines of a file
404+ async function headFile ( filePath : string , numLines : number ) : Promise < string > {
405+ const fileHandle = await fs . open ( filePath , 'r' ) ;
406+ try {
407+ const lines : string [ ] = [ ] ;
408+ let buffer = '' ;
409+ let bytesRead = 0 ;
410+ const chunk = Buffer . alloc ( 1024 ) ; // 1KB buffer
411+
412+ // Read chunks and count lines until we have enough or reach EOF
413+ while ( lines . length < numLines ) {
414+ const result = await fileHandle . read ( chunk , 0 , chunk . length , bytesRead ) ;
415+ if ( result . bytesRead === 0 ) break ; // End of file
416+ bytesRead += result . bytesRead ;
417+ buffer += chunk . slice ( 0 , result . bytesRead ) . toString ( 'utf-8' ) ;
418+
419+ const newLineIndex = buffer . lastIndexOf ( '\n' ) ;
420+ if ( newLineIndex !== - 1 ) {
421+ const completeLines = buffer . slice ( 0 , newLineIndex ) . split ( '\n' ) ;
422+ buffer = buffer . slice ( newLineIndex + 1 ) ;
423+ for ( const line of completeLines ) {
424+ lines . push ( line ) ;
425+ if ( lines . length >= numLines ) break ;
426+ }
427+ }
428+ }
429+
430+ // If there is leftover content and we still need lines, add it
431+ if ( buffer . length > 0 && lines . length < numLines ) {
432+ lines . push ( buffer ) ;
433+ }
434+
435+ return lines . join ( '\n' ) ;
436+ } finally {
437+ await fileHandle . close ( ) ;
438+ }
439+ }
440+
333441// Tool handlers
334442server . setRequestHandler ( ListToolsRequestSchema , async ( ) => {
335443 return {
@@ -340,7 +448,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
340448 "Read the complete contents of a file from the file system. " +
341449 "Handles various text encodings and provides detailed error messages " +
342450 "if the file cannot be read. Use this tool when you need to examine " +
343- "the contents of a single file. Only works within allowed directories." ,
451+ "the contents of a single file. Use the 'head' parameter to read only " +
452+ "the first N lines of a file, or the 'tail' parameter to read only " +
453+ "the last N lines of a file. Only works within allowed directories." ,
344454 inputSchema : zodToJsonSchema ( ReadFileArgsSchema ) as ToolInput ,
345455 } ,
346456 {
@@ -387,6 +497,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
387497 "finding specific files within a directory. Only works within allowed directories." ,
388498 inputSchema : zodToJsonSchema ( ListDirectoryArgsSchema ) as ToolInput ,
389499 } ,
500+ {
501+ name : "list_directory_with_sizes" ,
502+ description :
503+ "Get a detailed listing of all files and directories in a specified path, including sizes. " +
504+ "Results clearly distinguish between files and directories with [FILE] and [DIR] " +
505+ "prefixes. This tool is useful for understanding directory structure and " +
506+ "finding specific files within a directory. Only works within allowed directories." ,
507+ inputSchema : zodToJsonSchema ( ListDirectoryWithSizesArgsSchema ) as ToolInput ,
508+ } ,
390509 {
391510 name : "directory_tree" ,
392511 description :
@@ -451,6 +570,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
451570 throw new Error ( `Invalid arguments for read_file: ${ parsed . error } ` ) ;
452571 }
453572 const validPath = await validatePath ( parsed . data . path ) ;
573+
574+ if ( parsed . data . head && parsed . data . tail ) {
575+ throw new Error ( "Cannot specify both head and tail parameters simultaneously" ) ;
576+ }
577+
578+ if ( parsed . data . tail ) {
579+ // Use memory-efficient tail implementation for large files
580+ const tailContent = await tailFile ( validPath , parsed . data . tail ) ;
581+ return {
582+ content : [ { type : "text" , text : tailContent } ] ,
583+ } ;
584+ }
585+
586+ if ( parsed . data . head ) {
587+ // Use memory-efficient head implementation for large files
588+ const headContent = await headFile ( validPath , parsed . data . head ) ;
589+ return {
590+ content : [ { type : "text" , text : headContent } ] ,
591+ } ;
592+ }
593+
454594 const content = await fs . readFile ( validPath , "utf-8" ) ;
455595 return {
456596 content : [ { type : "text" , text : content } ] ,
@@ -530,11 +670,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
530670 } ;
531671 }
532672
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 } ` ) ;
673+ case "list_directory_with_sizes" : {
674+ const parsed = ListDirectoryWithSizesArgsSchema . safeParse ( args ) ;
675+ if ( ! parsed . success ) {
676+ throw new Error ( `Invalid arguments for list_directory_with_sizes: ${ parsed . error } ` ) ;
677+ }
678+ const validPath = await validatePath ( parsed . data . path ) ;
679+ const entries = await fs . readdir ( validPath , { withFileTypes : true } ) ;
680+
681+ // Get detailed information for each entry
682+ const detailedEntries = await Promise . all (
683+ entries . map ( async ( entry ) => {
684+ const entryPath = path . join ( validPath , entry . name ) ;
685+ try {
686+ const stats = await fs . stat ( entryPath ) ;
687+ return {
688+ name : entry . name ,
689+ isDirectory : entry . isDirectory ( ) ,
690+ size : stats . size ,
691+ mtime : stats . mtime
692+ } ;
693+ } catch ( error ) {
694+ return {
695+ name : entry . name ,
696+ isDirectory : entry . isDirectory ( ) ,
697+ size : 0 ,
698+ mtime : new Date ( 0 )
699+ } ;
537700 }
701+ } )
702+ ) ;
703+
704+ // Sort entries based on sortBy parameter
705+ const sortedEntries = [ ...detailedEntries ] . sort ( ( a , b ) => {
706+ if ( parsed . data . sortBy === 'size' ) {
707+ return b . size - a . size ; // Descending by size
708+ }
709+ // Default sort by name
710+ return a . name . localeCompare ( b . name ) ;
711+ } ) ;
712+
713+ // Format the output
714+ const formattedEntries = sortedEntries . map ( entry =>
715+ `${ entry . isDirectory ? "[DIR]" : "[FILE]" } ${ entry . name . padEnd ( 30 ) } ${
716+ entry . isDirectory ? "" : formatSize ( entry . size ) . padStart ( 10 )
717+ } `
718+ ) ;
719+
720+ // Add summary
721+ const totalFiles = detailedEntries . filter ( e => ! e . isDirectory ) . length ;
722+ const totalDirs = detailedEntries . filter ( e => e . isDirectory ) . length ;
723+ const totalSize = detailedEntries . reduce ( ( sum , entry ) => sum + ( entry . isDirectory ? 0 : entry . size ) , 0 ) ;
724+
725+ const summary = [
726+ "" ,
727+ `Total: ${ totalFiles } files, ${ totalDirs } directories` ,
728+ `Combined size: ${ formatSize ( totalSize ) } `
729+ ] ;
730+
731+ return {
732+ content : [ {
733+ type : "text" ,
734+ text : [ ...formattedEntries , ...summary ] . join ( "\n" )
735+ } ] ,
736+ } ;
737+ }
738+
739+ case "directory_tree" : {
740+ const parsed = DirectoryTreeArgsSchema . safeParse ( args ) ;
741+ if ( ! parsed . success ) {
742+ throw new Error ( `Invalid arguments for directory_tree: ${ parsed . error } ` ) ;
743+ }
538744
539745 interface TreeEntry {
540746 name: string ;
0 commit comments