@@ -14,7 +14,6 @@ import { createReadStream } from "fs";
1414import path from "path" ;
1515import { z } from "zod" ;
1616import { zodToJsonSchema } from "zod-to-json-schema" ;
17- import { minimatch } from "minimatch" ;
1817import { normalizePath , expandHome } from './path-utils.js' ;
1918import { getValidRootDirectories } from './roots-utils.js' ;
2019import {
@@ -39,6 +38,7 @@ if (args.length === 0) {
3938 console . error ( " 1. Command-line arguments (shown above)" ) ;
4039 console . error ( " 2. MCP roots protocol (if client supports it)" ) ;
4140 console . error ( "At least one directory must be provided by EITHER method for the server to operate." ) ;
41+ console . error ( "Note: Directories will be validated at startup but operations will be retried at runtime." ) ;
4242}
4343
4444// Store allowed directories in normalized and resolved form
@@ -59,22 +59,33 @@ let allowedDirectories = await Promise.all(
5959 } )
6060) ;
6161
62- // Validate that all directories exist and are accessible
63- await Promise . all ( allowedDirectories . map ( async ( dir ) => {
64- try {
65- const stats = await fs . stat ( dir ) ;
66- if ( ! stats . isDirectory ( ) ) {
67- console . error ( `Error: ${ dir } is not a directory` ) ;
68- process . exit ( 1 ) ;
62+ // Validate directories at startup - log warnings if path is not accessible at startup.
63+ // Directory accessibility may change between startup and runtime.
64+ const validatedDirectories = await Promise . all (
65+ allowedDirectories . map ( async ( dir ) => {
66+ try {
67+ const stats = await fs . stat ( dir ) ;
68+ if ( stats . isDirectory ( ) ) {
69+ console . error ( `Directory accessible: ${ dir } ` ) ;
70+ return dir ;
71+ } else if ( stats . isFile ( ) ) {
72+ console . error ( `${ dir } is a file, not a directory - skipping` ) ;
73+ return null ;
74+ } else {
75+ // Include symlinks/special files - they might become directories when NAS/VPN reconnects
76+ console . error ( `${ dir } is not a directory (${ stats . isSymbolicLink ( ) ? 'symlink' : 'special file' } )` ) ;
77+ return dir ;
78+ }
79+ } catch ( error ) {
80+ // Include inaccessible paths - they might become accessible when storage/network reconnects
81+ console . error ( `Directory not accessible: ${ dir } - ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
82+ return dir ;
6983 }
70- } catch ( error ) {
71- console . error ( `Error accessing directory ${ dir } :` , error ) ;
72- process . exit ( 1 ) ;
73- }
74- } ) ) ;
84+ } )
85+ ) . then ( results => results . filter ( ( dir ) : dir is string => dir !== null ) ) ;
7586
7687// Initialize the global allowedDirectories in lib.ts
77- setAllowedDirectories ( allowedDirectories ) ;
88+ setAllowedDirectories ( validatedDirectories ) ;
7889
7990// Schema definitions
8091const ReadTextFileArgsSchema = z . object ( {
@@ -88,10 +99,7 @@ const ReadMediaFileArgsSchema = z.object({
8899} ) ;
89100
90101const ReadMultipleFilesArgsSchema = z . object ( {
91- paths : z
92- . array ( z . string ( ) )
93- . min ( 1 , "At least one file path must be provided" )
94- . describe ( "Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories." ) ,
102+ paths : z . array ( z . string ( ) ) ,
95103} ) ;
96104
97105const WriteFileArgsSchema = z . object ( {
@@ -105,7 +113,7 @@ const EditOperation = z.object({
105113} ) ;
106114
107115const EditFileArgsSchema = z . object ( {
108- path : z . string ( ) . describe ( 'File path to edit' ) ,
116+ path : z . string ( ) ,
109117 edits : z . array ( EditOperation ) ,
110118 dryRun : z . boolean ( ) . default ( false ) . describe ( 'Preview changes using git-style diff format' )
111119} ) ;
@@ -125,22 +133,17 @@ const ListDirectoryWithSizesArgsSchema = z.object({
125133
126134const DirectoryTreeArgsSchema = z . object ( {
127135 path : z . string ( ) ,
128- excludePatterns : z . array ( z . string ( ) ) . optional ( ) . default ( [ ] )
129136} ) ;
130137
131138const MoveFileArgsSchema = z . object ( {
132139 source : z . string ( ) ,
133140 destination : z . string ( ) ,
134141} ) ;
135142
136- const SearchFilesByNameArgsSchema = z . object ( {
137- path : z . string ( ) . describe ( 'The root directory path to start the search from' ) ,
138- pattern : z . string ( ) . describe ( 'Pattern to match within file/directory names. Supports glob patterns. ' +
139- 'Case insensitive unless pattern contains uppercase characters' ) ,
140- excludePatterns : z . array ( z . string ( ) )
141- . optional ( )
142- . default ( [ ] )
143- . describe ( 'Glob patterns for paths to exclude from search (e.g., "node_modules/**")' )
143+ const SearchFilesArgsSchema = z . object ( {
144+ path : z . string ( ) ,
145+ pattern : z . string ( ) ,
146+ excludePatterns : z . array ( z . string ( ) ) . optional ( ) . default ( [ ] )
144147} ) ;
145148
146149const GetFileInfoArgsSchema = z . object ( {
@@ -149,6 +152,7 @@ const GetFileInfoArgsSchema = z.object({
149152
150153const ToolInputSchema = ToolSchema . shape . inputSchema ;
151154type ToolInput = z . infer < typeof ToolInputSchema > ;
155+
152156// Server setup
153157const server = new Server (
154158 {
@@ -280,14 +284,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
280284 inputSchema : zodToJsonSchema ( MoveFileArgsSchema ) as ToolInput ,
281285 } ,
282286 {
283- name : "search_files_by_name " ,
287+ name : "search_files " ,
284288 description :
285- "Recursively search for files and directories by name matching a glob pattern. " +
286- "Supports glob-style patterns that match paths relative to the working directory. " +
287- "Use patterns like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " +
288- "Returns full paths to all matching items. Great for finding files when you don't know their exact location. " +
289+ "Recursively search for files and directories matching a pattern. " +
290+ "Searches through all subdirectories from the starting path. The search " +
291+ "is case-insensitive and matches partial names. Returns full paths to all " +
292+ "matching items. Great for finding files when you don't know their exact location. " +
289293 "Only searches within allowed directories." ,
290- inputSchema : zodToJsonSchema ( SearchFilesByNameArgsSchema ) as ToolInput ,
294+ inputSchema : zodToJsonSchema ( SearchFilesArgsSchema ) as ToolInput ,
291295 } ,
292296 {
293297 name : "get_file_info" ,
@@ -536,36 +540,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
536540 type: 'file' | 'directory' ;
537541 children ?: TreeEntry [ ] ;
538542 }
539- const rootPath = parsed . data . path ;
540543
541- async function buildTree ( currentPath : string , excludePatterns : string [ ] = [ ] ) : Promise < TreeEntry [ ] > {
544+ async function buildTree ( currentPath : string ) : Promise < TreeEntry [ ] > {
542545 const validPath = await validatePath ( currentPath ) ;
543546 const entries = await fs . readdir ( validPath , { withFileTypes : true } ) ;
544547 const result : TreeEntry [ ] = [ ] ;
545548
546549 for ( const entry of entries ) {
547- const relativePath = path . relative ( rootPath , path . join ( currentPath , entry . name ) ) ;
548- const shouldExclude = excludePatterns . some ( pattern => {
549- if ( pattern . includes ( '*' ) ) {
550- return minimatch ( relativePath , pattern , { dot : true } ) ;
551- }
552- // For files: match exact name or as part of path
553- // For directories: match as directory path
554- return minimatch ( relativePath , pattern , { dot : true } ) ||
555- minimatch ( relativePath , `**/${ pattern } ` , { dot : true } ) ||
556- minimatch ( relativePath , `**/${ pattern } /**` , { dot : true } ) ;
557- } ) ;
558- if ( shouldExclude )
559- continue ;
560-
561550 const entryData : TreeEntry = {
562551 name : entry . name ,
563552 type : entry . isDirectory ( ) ? 'directory' : 'file'
564553 } ;
565554
566555 if ( entry . isDirectory ( ) ) {
567556 const subPath = path . join ( currentPath , entry . name ) ;
568- entryData . children = await buildTree ( subPath , excludePatterns ) ;
557+ entryData . children = await buildTree ( subPath ) ;
569558 }
570559
571560 result . push ( entryData ) ;
@@ -574,7 +563,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
574563 return result ;
575564 }
576565
577- const treeData = await buildTree ( rootPath , parsed . data . excludePatterns ) ;
566+ const treeData = await buildTree ( parsed . data . path ) ;
578567 return {
579568 content : [ {
580569 type : "text" ,
@@ -596,10 +585,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
596585 } ;
597586 }
598587
599- case "search_files_by_name ": {
600- const parsed = SearchFilesByNameArgsSchema . safeParse ( args ) ;
588+ case "search_files ": {
589+ const parsed = SearchFilesArgsSchema . safeParse ( args ) ;
601590 if ( ! parsed . success ) {
602- throw new Error ( `Invalid arguments for search_files_by_name : ${ parsed . error } ` ) ;
591+ throw new Error ( `Invalid arguments for search_files : ${ parsed . error } ` ) ;
603592 }
604593 const validPath = await validatePath ( parsed . data . path ) ;
605594 const results = await searchFilesByName ( validPath , parsed . data . pattern , parsed . data . excludePatterns ) ;
@@ -616,11 +605,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
616605 const validPath = await validatePath ( parsed . data . path ) ;
617606 const info = await getFileStats ( validPath ) ;
618607 return {
619- content : [ {
620- type : "text" , text : Object . entries ( info )
621- . map ( ( [ key , value ] ) => `${ key } : ${ value } ` )
622- . join ( "\n" )
623- } ] ,
608+ content : [ { type : "text" , text : Object . entries ( info )
609+ . map ( ( [ key , value ] ) => `${ key } : ${ value } ` )
610+ . join ( "\n" ) } ] ,
624611 } ;
625612 }
626613
0 commit comments