Skip to main content
Glama
MartinSchlott

BetterMCPFileServer

searchTools.ts11.9 kB
import fs from "fs/promises"; import path from "path"; import glob from "fast-glob"; import { z } from "zod"; import { validatePath, pathAliases, toAliasPath, resolveAliasPath, normalizePath } from "../pathUtils.js"; // Schema definition export const SearchFilesAndFoldersSchema = z.object({ pattern: z.string(), includeMetadata: z.boolean().optional().default(false), ignore: z.array(z.string()).optional(), cwd: z.string().optional(), }); export async function searchFilesAndFolders(parsed: z.infer<typeof SearchFilesAndFoldersSchema>, allowedDirectories: string[]): Promise<any[]> { // Default ignore patterns similar to .gitignore const defaultIgnore = [ "**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.DS_Store", ]; const ignorePatterns = parsed.ignore || defaultIgnore; let searchResults: string[] = []; // Special case: Root-level listing with "*" pattern // This should only return the top-level directories, not recursively search if (parsed.pattern === "*" && (!parsed.cwd || parsed.cwd === 'root' || parsed.cwd === '/') && pathAliases.length > 0) { console.error("Special case: Root-level listing with '*' pattern"); // Instead of searching, we'll just return the aliases as top-level entries const results = pathAliases.map(alias => ({ path: alias.alias, // Always include type type: "directory", // Only include these additional fields if metadata was requested ...(parsed.includeMetadata ? { size: 0, created: new Date(), modified: new Date(), } : {}) })); return results; } // Determine if the pattern starts with a specific alias const patternParts = parsed.pattern.split(/[\/\\]/, 2); const potentialAlias = patternParts[0]; const hasSpecificAlias = pathAliases.some(pa => pa.alias === potentialAlias); // Case 1: Pattern starts with a specific alias (e.g., "docs/file.txt") if (hasSpecificAlias) { // The pattern already contains an alias, resolve it and search try { // Check if the pattern contains glob wildcards const hasGlobWildcards = parsed.pattern.includes('*') || parsed.pattern.includes('?') || parsed.pattern.includes('{') || parsed.pattern.includes('['); if (hasGlobWildcards) { // Extract the alias and get its full path const matchingAlias = pathAliases.find(pa => pa.alias === potentialAlias); if (matchingAlias) { // For patterns like "MyWritings/**/jokes.md", we need to extract everything after the alias const aliasWithSlash = potentialAlias + '/'; const remainingPattern = parsed.pattern.startsWith(aliasWithSlash) ? parsed.pattern.substring(aliasWithSlash.length) : '**'; // If just the alias is given, search everything // Special case for "MyWritings/*" pattern - only list top level if (remainingPattern === "*") { console.error(`Special case: Top-level listing for "${potentialAlias}/*" pattern`); try { // Get direct children only const entries = await fs.readdir(matchingAlias.fullPath, { withFileTypes: true }); searchResults = entries.map(entry => { return path.join(matchingAlias.fullPath, entry.name); }); console.error(`Found ${searchResults.length} top-level items in ${matchingAlias.alias}`); } catch (error) { console.error(`Error reading directory ${matchingAlias.fullPath}:`, error); } } else { // Regular glob pattern search console.error(`Searching for glob pattern "${remainingPattern}" in alias ${potentialAlias}`); // Search with the glob pattern in the alias path searchResults = await glob(remainingPattern, { cwd: matchingAlias.fullPath, ignore: ignorePatterns, absolute: true, onlyFiles: false, dot: true, }); console.error(`Found ${searchResults.length} results for "${remainingPattern}" in ${matchingAlias.alias}`); } // If nothing was found but we were looking for a specific file pattern, // try checking if the file exists directly (in case glob had issues) if (searchResults.length === 0 && !remainingPattern.includes('**')) { const directPath = path.join(matchingAlias.fullPath, remainingPattern); try { await fs.access(directPath); const stats = await fs.stat(directPath); if (stats.isFile() || stats.isDirectory()) { console.error(`Found direct match at ${directPath}`); searchResults = [directPath]; } } catch (error) { // File doesn't exist directly console.error(`No direct match found at ${directPath}`); } } } } else { // No glob wildcards, treat as a normal path const resolvedPath = resolveAliasPath(parsed.pattern); // If the pattern points to a specific file, just validate and return it try { const stats = await fs.stat(resolvedPath); if (stats.isFile()) { searchResults = [resolvedPath]; } else { // It's a directory, so use it as cwd and search with remaining pattern const remainingPattern = patternParts.length > 1 ? patternParts.slice(1).join('/') : '**'; searchResults = await glob(remainingPattern, { cwd: resolvedPath, ignore: ignorePatterns, absolute: true, onlyFiles: false, dot: true, }); } } catch (error) { // Path doesn't exist, use the directory part as cwd and rest as pattern const dirPart = path.dirname(resolvedPath); const filePart = path.basename(parsed.pattern); try { await fs.access(dirPart); searchResults = await glob(filePart, { cwd: dirPart, ignore: ignorePatterns, absolute: true, onlyFiles: false, dot: true, }); } catch { // Even the directory doesn't exist console.error(`Neither path nor parent directory exists: ${resolvedPath}`); } } } } catch (error) { console.error(`Error resolving alias path: ${error}`); } } // Case 2: User specified a cwd else if (parsed.cwd) { // Skip the special case already handled above if ((parsed.cwd === 'root' || parsed.cwd === '/') && parsed.pattern === '*') { // This is handled by the special case above, do nothing here console.error("Root listing with '*' pattern already handled"); } else if (parsed.cwd === 'root' || parsed.cwd === '/') { throw new Error("Cannot use root as the current working directory for search with patterns other than '*'"); } else { // Resolve the cwd as an aliased path first const cwd = await validatePath(parsed.cwd, allowedDirectories); // Special case for "*" pattern with cwd - only list top level if (parsed.pattern === "*") { console.error(`Special case: Top-level listing for cwd "${parsed.cwd}" with "*" pattern`); try { // Get direct children only const entries = await fs.readdir(cwd, { withFileTypes: true }); searchResults = entries.map(entry => { return path.join(cwd, entry.name); }); console.error(`Found ${searchResults.length} top-level items in ${parsed.cwd}`); } catch (error) { console.error(`Error reading directory ${cwd}:`, error); } } else { // Regular search in the specified cwd searchResults = await glob(parsed.pattern, { cwd, ignore: ignorePatterns, absolute: true, onlyFiles: false, dot: true, }); } } } // Case 3: No cwd, no specific alias in pattern - search ALL aliases else if (pathAliases.length > 0) { // Special handling for "*" pattern has already been done at the top of the function if (parsed.pattern !== "*") { console.error(`Searching pattern "${parsed.pattern}" across all aliases`); // Search in ALL aliased directories for (const alias of pathAliases) { try { const aliasResults = await glob(parsed.pattern, { cwd: alias.fullPath, ignore: ignorePatterns, absolute: true, onlyFiles: false, dot: true, }); searchResults = [...searchResults, ...aliasResults]; } catch (error) { console.error(`Error searching in ${alias.alias}:`, error); } } } } // Case 4: Legacy mode (no aliases defined) else { // Fallback to process.cwd() only if not using aliases const cwd = process.cwd(); // Special case for "*" pattern - only list top level if (parsed.pattern === "*") { console.error(`Special case: Top-level listing for current directory with "*" pattern`); try { // Get direct children only const entries = await fs.readdir(cwd, { withFileTypes: true }); searchResults = entries.map(entry => { return path.join(cwd, entry.name); }); console.error(`Found ${searchResults.length} top-level items in current directory`); } catch (error) { console.error(`Error reading directory ${cwd}:`, error); } } else { // Regular search in current directory searchResults = await glob(parsed.pattern, { cwd, ignore: ignorePatterns, absolute: true, onlyFiles: false, dot: true, }); } } // Filter out any matches that are outside allowed directories // and optionally add metadata const results = await Promise.all( searchResults.map(async (match) => { try { // For searches with aliases, we need to check if the match is within an allowed directory if (pathAliases.length > 0) { const normalizedMatch = normalizePath(match); const isAllowed = pathAliases.some(alias => normalizedMatch.startsWith(alias.normalizedPath) ); if (!isAllowed) { return null; // Skip files outside allowed directories } } else { // For non-alias mode, use validatePath await validatePath(match, allowedDirectories); } // Convert to alias path for the response const aliasPath = pathAliases.length > 0 ? toAliasPath(match) : match; // Get file stats - needed for type determination regardless of metadata option const stats = await fs.stat(match); const fileType = stats.isDirectory() ? "directory" : "file"; if (parsed.includeMetadata) { return { path: aliasPath, type: fileType, size: stats.size, created: stats.birthtime, modified: stats.mtime, }; } else { return { path: aliasPath, type: fileType }; } } catch (error) { console.error(`Error processing search result ${match}:`, error); return null; } }) ); // Filter out null values from paths that failed validation return results.filter(Boolean); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MartinSchlott/BetterMCPFileServer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server