From 8c6913a074d791ccc61124d702df00bfe6140c19 Mon Sep 17 00:00:00 2001 From: "finn.andersen" Date: Wed, 5 Mar 2025 21:13:36 +0100 Subject: [PATCH 1/5] Support glob pattern in search_files tool --- src/filesystem/index.ts | 104 ++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index b4d5c419fc..697e5a8055 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -205,16 +205,16 @@ async function searchFiles( // Check if path matches any exclude pattern const relativePath = path.relative(rootPath, fullPath); - const shouldExclude = excludePatterns.some(pattern => { - const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`; - return minimatch(relativePath, globPattern, { dot: true }); - }); + const shouldExclude = excludePatterns.some(pattern => + minimatch(relativePath, pattern, { dot: true }) + ); if (shouldExclude) { continue; } - if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { + // Use glob matching for the search pattern as well + if (minimatch(relativePath, pattern, { dot: true })) { results.push(fullPath); } @@ -254,7 +254,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath async function applyFileEdits( filePath: string, - edits: Array<{oldText: string, newText: string}>, + edits: Array<{ oldText: string, newText: string }>, dryRun = false ): Promise { // Read file content and normalize line endings @@ -390,10 +390,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { { name: "directory_tree", description: - "Get a recursive tree view of files and directories as a JSON structure. " + - "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + - "Files have no children array, while directories always have a children array (which may be empty). " + - "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", + "Get a recursive tree view of files and directories as a JSON structure. " + + "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + + "Files have no children array, while directories always have a children array (which may be empty). " + + "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput, }, { @@ -409,9 +409,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "search_files", description: "Recursively search for files and directories matching a pattern. " + - "Searches through all subdirectories from the starting path. The search " + - "is case-insensitive and matches partial names. Returns full paths to all " + - "matching items. Great for finding files when you don't know their exact location. " + + "The patterns should be glob-style patterns that match paths relative to the working directory. " + + "Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " + + "Returns full paths to all matching items. Great for finding files when you don't know their exact location. " + "Only searches within allowed directories.", inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput, }, @@ -530,49 +530,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } - case "directory_tree": { - const parsed = DirectoryTreeArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`); - } - - interface TreeEntry { - name: string; - type: 'file' | 'directory'; - children?: TreeEntry[]; - } - - async function buildTree(currentPath: string): Promise { - const validPath = await validatePath(currentPath); - const entries = await fs.readdir(validPath, {withFileTypes: true}); - const result: TreeEntry[] = []; + case "directory_tree": { + const parsed = DirectoryTreeArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`); + } - for (const entry of entries) { - const entryData: TreeEntry = { - name: entry.name, - type: entry.isDirectory() ? 'directory' : 'file' - }; + interface TreeEntry { + name: string; + type: 'file' | 'directory'; + children?: TreeEntry[]; + } - if (entry.isDirectory()) { - const subPath = path.join(currentPath, entry.name); - entryData.children = await buildTree(subPath); - } + async function buildTree(currentPath: string): Promise { + const validPath = await validatePath(currentPath); + const entries = await fs.readdir(validPath, { withFileTypes: true }); + const result: TreeEntry[] = []; - result.push(entryData); - } + for (const entry of entries) { + const entryData: TreeEntry = { + name: entry.name, + type: entry.isDirectory() ? 'directory' : 'file' + }; - return result; + if (entry.isDirectory()) { + const subPath = path.join(currentPath, entry.name); + entryData.children = await buildTree(subPath); } - const treeData = await buildTree(parsed.data.path); - return { - content: [{ - type: "text", - text: JSON.stringify(treeData, null, 2) - }], - }; + result.push(entryData); + } + + return result; } + const treeData = await buildTree(parsed.data.path); + return { + content: [{ + type: "text", + text: JSON.stringify(treeData, null, 2) + }], + }; + } + case "move_file": { const parsed = MoveFileArgsSchema.safeParse(args); if (!parsed.success) { @@ -606,9 +606,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const validPath = await validatePath(parsed.data.path); const info = await getFileStats(validPath); return { - content: [{ type: "text", text: Object.entries(info) - .map(([key, value]) => `${key}: ${value}`) - .join("\n") }], + content: [{ + type: "text", text: Object.entries(info) + .map(([key, value]) => `${key}: ${value}`) + .join("\n") + }], }; } From 6f096f93089a9a2efeb1bc3b2c1bc538bdc3e806 Mon Sep 17 00:00:00 2001 From: "finn.andersen" Date: Wed, 5 Mar 2025 23:52:20 +0100 Subject: [PATCH 2/5] Update README --- src/filesystem/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/filesystem/README.md b/src/filesystem/README.md index c52f1a4041..2e26e9cf45 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -78,12 +78,12 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio - Fails if destination exists - **search_files** - - Recursively search for files/directories + - Recursively search for files/directories that match or do not match patterns - Inputs: - `path` (string): Starting directory - `pattern` (string): Search pattern - - `excludePatterns` (string[]): Exclude any patterns. Glob formats are supported. - - Case-insensitive matching + - `excludePatterns` (string[]): Exclude any patterns. + - Glob-style pattern matching - Returns full paths to matches - **get_file_info** From 77b2566ddeb18c86dc5d2bafbed780f484eaad7b Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Fri, 15 Aug 2025 17:31:47 +0100 Subject: [PATCH 3/5] Remove unrelated indentation changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep only the core glob pattern functionality for search_files. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/filesystem/index.ts | 78 ++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 01ee98089f..c476982295 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -287,7 +287,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath async function applyFileEdits( filePath: string, - edits: Array<{ oldText: string, newText: string }>, + edits: Array<{oldText: string, newText: string}>, dryRun = false ): Promise { // Read file content and normalize line endings @@ -578,10 +578,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { { name: "directory_tree", description: - "Get a recursive tree view of files and directories as a JSON structure. " + - "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + - "Files have no children array, while directories always have a children array (which may be empty). " + - "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", + "Get a recursive tree view of files and directories as a JSON structure. " + + "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + + "Files have no children array, while directories always have a children array (which may be empty). " + + "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput, }, { @@ -868,42 +868,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`); } - interface TreeEntry { - name: string; - type: 'file' | 'directory'; - children?: TreeEntry[]; - } + interface TreeEntry { + name: string; + type: 'file' | 'directory'; + children?: TreeEntry[]; + } - async function buildTree(currentPath: string): Promise { - const validPath = await validatePath(currentPath); - const entries = await fs.readdir(validPath, { withFileTypes: true }); - const result: TreeEntry[] = []; + async function buildTree(currentPath: string): Promise { + const validPath = await validatePath(currentPath); + const entries = await fs.readdir(validPath, {withFileTypes: true}); + const result: TreeEntry[] = []; - for (const entry of entries) { - const entryData: TreeEntry = { - name: entry.name, - type: entry.isDirectory() ? 'directory' : 'file' - }; + for (const entry of entries) { + const entryData: TreeEntry = { + name: entry.name, + type: entry.isDirectory() ? 'directory' : 'file' + }; - if (entry.isDirectory()) { - const subPath = path.join(currentPath, entry.name); - entryData.children = await buildTree(subPath); - } + if (entry.isDirectory()) { + const subPath = path.join(currentPath, entry.name); + entryData.children = await buildTree(subPath); + } - result.push(entryData); - } + result.push(entryData); + } - return result; - } + return result; + } - const treeData = await buildTree(parsed.data.path); - return { - content: [{ - type: "text", - text: JSON.stringify(treeData, null, 2) - }], - }; - } + const treeData = await buildTree(parsed.data.path); + return { + content: [{ + type: "text", + text: JSON.stringify(treeData, null, 2) + }], + }; + } case "move_file": { const parsed = MoveFileArgsSchema.safeParse(args); @@ -938,11 +938,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const validPath = await validatePath(parsed.data.path); const info = await getFileStats(validPath); return { - content: [{ - type: "text", text: Object.entries(info) - .map(([key, value]) => `${key}: ${value}`) - .join("\n") - }], + content: [{ type: "text", text: Object.entries(info) + .map(([key, value]) => `${key}: ${value}`) + .join("\n") }], }; } From a2c66bad980d18488a5eb3c18dbf7decccce947f Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Sat, 23 Aug 2025 06:33:58 +0000 Subject: [PATCH 4/5] fix: implement glob pattern matching for search_files pattern argument - Use minimatch for pattern matching instead of substring matching - Remove automatic conversion of excludePatterns to **//** format - Both pattern and excludePatterns now use consistent glob matching --- src/filesystem/lib.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index 40cb316e53..240ca0d476 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -367,14 +367,14 @@ export async function searchFilesWithValidation( await validatePath(fullPath); const relativePath = path.relative(rootPath, fullPath); - const shouldExclude = excludePatterns.some(excludePattern => { - const globPattern = excludePattern.includes('*') ? excludePattern : `**/${excludePattern}/**`; - return minimatch(relativePath, globPattern, { dot: true }); - }); + const shouldExclude = excludePatterns.some(excludePattern => + minimatch(relativePath, excludePattern, { dot: true }) + ); if (shouldExclude) continue; - if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { + // Use glob matching for the search pattern + if (minimatch(relativePath, pattern, { dot: true })) { results.push(fullPath); } From 36e8dc5bbe45f1a13911adfa326e1aec5d4a4a8f Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Sat, 23 Aug 2025 06:35:12 +0000 Subject: [PATCH 5/5] test: update search tests to use glob patterns Tests now use '*test*' pattern instead of 'test' to match the new glob-based search behavior --- src/filesystem/__tests__/lib.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index 76d3679280..cc13ef0353 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -316,7 +316,7 @@ describe('Lib Functions', () => { const result = await searchFilesWithValidation( testDir, - 'test', + '*test*', allowedDirs, { excludePatterns: ['*.log', 'node_modules'] } ); @@ -346,7 +346,7 @@ describe('Lib Functions', () => { const result = await searchFilesWithValidation( testDir, - 'test', + '*test*', allowedDirs, {} ); @@ -370,7 +370,7 @@ describe('Lib Functions', () => { const result = await searchFilesWithValidation( testDir, - 'test', + '*test*', allowedDirs, { excludePatterns: ['*.backup'] } );