Skip to content

Commit c0478e9

Browse files
Use searchFilesByName and remove searchFilesWithValidation.
1 parent 56a4962 commit c0478e9

3 files changed

Lines changed: 54 additions & 82 deletions

File tree

src/filesystem/__tests__/lib.test.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
readFileContent,
1717
writeFileContent,
1818
// Search & filtering functions
19-
searchFilesWithValidation,
2019
searchFilesByName,
2120
// File editing functions
2221
applyFileEdits,
@@ -310,7 +309,7 @@ describe('Lib Functions', () => {
310309
});
311310

312311
describe('Search & Filtering Functions', () => {
313-
describe('searchFilesWithValidation', () => {
312+
describe('searchFilesByName', () => {
314313
beforeEach(() => {
315314
mockFs.realpath.mockImplementation(async (path: any) => path.toString());
316315
});
@@ -327,11 +326,10 @@ describe('Lib Functions', () => {
327326
const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir';
328327
const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed'];
329328

330-
const result = await searchFilesWithValidation(
329+
const result = await searchFilesByName(
331330
testDir,
332331
'*test*',
333-
allowedDirs,
334-
{ excludePatterns: ['*.log', 'node_modules'] }
332+
['*.log', 'node_modules']
335333
);
336334

337335
const expectedResult = process.platform === 'win32' ? 'C:\\allowed\\dir\\test.txt' : '/allowed/dir/test.txt';
@@ -357,11 +355,10 @@ describe('Lib Functions', () => {
357355
const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir';
358356
const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed'];
359357

360-
const result = await searchFilesWithValidation(
358+
const result = await searchFilesByName(
361359
testDir,
362360
'*test*',
363-
allowedDirs,
364-
{}
361+
[]
365362
);
366363

367364
// Should only return the valid file, skipping the invalid one
@@ -381,11 +378,10 @@ describe('Lib Functions', () => {
381378
const testDir = process.platform === 'win32' ? 'C:\\allowed\\dir' : '/allowed/dir';
382379
const allowedDirs = process.platform === 'win32' ? ['C:\\allowed'] : ['/allowed'];
383380

384-
const result = await searchFilesWithValidation(
381+
const result = await searchFilesByName(
385382
testDir,
386383
'*test*',
387-
allowedDirs,
388-
{ excludePatterns: ['*.backup'] }
384+
['*.backup']
389385
);
390386

391387
const expectedResults = process.platform === 'win32' ? [

src/filesystem/index.ts

Lines changed: 47 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { createReadStream } from "fs";
1414
import path from "path";
1515
import { z } from "zod";
1616
import { zodToJsonSchema } from "zod-to-json-schema";
17-
import { minimatch } from "minimatch";
1817
import { normalizePath, expandHome } from './path-utils.js';
1918
import { getValidRootDirectories } from './roots-utils.js';
2019
import {
@@ -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
8091
const ReadTextFileArgsSchema = z.object({
@@ -88,10 +99,7 @@ const ReadMediaFileArgsSchema = z.object({
8899
});
89100

90101
const 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

97105
const WriteFileArgsSchema = z.object({
@@ -105,7 +113,7 @@ const EditOperation = z.object({
105113
});
106114

107115
const 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

126134
const DirectoryTreeArgsSchema = z.object({
127135
path: z.string(),
128-
excludePatterns: z.array(z.string()).optional().default([])
129136
});
130137

131138
const 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

146149
const GetFileInfoArgsSchema = z.object({
@@ -149,6 +152,7 @@ const GetFileInfoArgsSchema = z.object({
149152

150153
const ToolInputSchema = ToolSchema.shape.inputSchema;
151154
type ToolInput = z.infer<typeof ToolInputSchema>;
155+
152156
// Server setup
153157
const 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

src/filesystem/lib.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -455,14 +455,3 @@ export async function searchFilesByName(
455455

456456
return results;
457457
}
458-
459-
// Legacy function for backward compatibility
460-
export async function searchFilesWithValidation(
461-
rootPath: string,
462-
pattern: string,
463-
allowedDirectories: string[],
464-
options: SearchOptions = {}
465-
): Promise<string[]> {
466-
const { excludePatterns = [] } = options;
467-
return searchFilesByName(rootPath, pattern, excludePatterns);
468-
}

0 commit comments

Comments
 (0)