Skip to main content
Glama
filesystem.ts8.06 kB
import { z } from "zod"; import fs from "fs"; import path from "path"; /** * File System Tools - Safe interaction with the file system */ // Security: Blocked patterns to prevent credential leakage const BLOCKED_PATTERNS = [ ".env", ".env.local", ".env.production", ".env.development", ".pem", ".key", ".p12", ".pfx", "id_rsa", "id_ed25519", ".npmrc", ".pypirc", "credentials", "secrets", ".aws/credentials", ".docker/config.json", ]; function isSensitivePath(filePath: string): boolean { const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/"); const basename = path.basename(filePath).toLowerCase(); return BLOCKED_PATTERNS.some( (pattern) => basename === pattern.toLowerCase() || basename.endsWith(pattern.toLowerCase()) || normalizedPath.includes(pattern.toLowerCase()), ); } // ============================================ // List Files // ============================================ export const listFilesSchema = { name: "list_files", description: "Lists files and directories in a path", inputSchema: z.object({ path: z.string().describe("Directory path to list"), recursive: z.boolean().optional().default(false), }), }; export async function listFilesHandler(args: { path: string; recursive?: boolean; }) { try { const entries = fs.readdirSync(args.path, { withFileTypes: true, recursive: args.recursive, }); const files = entries .filter( (e: any) => !isSensitivePath(path.join(e.path || args.path, e.name)), ) .map((e: any) => ({ name: e.name, type: e.isDirectory() ? "dir" : "file", path: path.join(e.path || args.path, e.name), })); return { content: [ { type: "text", text: JSON.stringify(files, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error listing files: ${(error as Error).message}`, }, ], isError: true, }; } } // ============================================ // Read File Snippet // ============================================ export const readFileSnippetSchema = { name: "read_file_snippet", description: "Reads a specific range of lines from a file", inputSchema: z.object({ filePath: z.string(), startLine: z.number().min(1).optional(), endLine: z.number().min(1).optional(), }), }; export async function readFileSnippetHandler(args: { filePath: string; startLine?: number; endLine?: number; }) { // Security: Block sensitive files if (isSensitivePath(args.filePath)) { return { content: [ { type: "text", text: `Access denied: Cannot read sensitive files.` }, ], isError: true, }; } try { const content = await fs.promises.readFile(args.filePath, "utf-8"); const lines = content.split("\n"); const start = (args.startLine || 1) - 1; const end = args.endLine || lines.length; const snippet = lines.slice(start, end).join("\n"); return { content: [ { type: "text", text: `\`\`\`\n${snippet}\n\`\`\``, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error reading file: ${(error as Error).message}`, }, ], isError: true, }; } } // ============================================ // Search Files // ============================================ export const searchFilesSchema = { name: "search_files", description: "Searches for a text pattern across files in a directory", inputSchema: z.object({ path: z.string(), pattern: z.string(), exclude: z.array(z.string()).optional(), }), }; export async function searchFilesHandler(args: { path: string; pattern: string; exclude?: string[]; }) { try { const results: { file: string; line: number; content: string }[] = []; const patternRegex = new RegExp(args.pattern, "i"); // Case-insensitive basic regex async function searchRecursively(dir: string) { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); // Skip excluded if ( args.exclude?.some((ex) => fullPath.includes(ex) || entry.name === ex) ) continue; if (entry.name === "node_modules" || entry.name === ".git") continue; if (entry.isDirectory()) { await searchRecursively(fullPath); } else if (entry.isFile()) { // Security: Skip sensitive files if (isSensitivePath(fullPath)) continue; try { const content = await fs.promises.readFile(fullPath, "utf-8"); const lines = content.split("\n"); lines.forEach((line: string, index: number) => { if (patternRegex.test(line)) { results.push({ file: fullPath, line: index + 1, content: line.trim(), }); } }); } catch { // Ignore binary or unreadable files } } } } await searchRecursively(args.path); return { content: [ { type: "text", text: results.length > 0 ? `# Search Results for "${args.pattern}"\n\n${results.map((r) => `- **${path.basename(r.file)}:${r.line}**: \`${r.content}\``).join("\n")}` : `No matches found for "${args.pattern}".`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error searching files: ${(error as Error).message}`, }, ], isError: true, }; } } // ============================================ // File Tree // ============================================ export const fileTreeSchema = { name: "get_file_tree", description: "Generates a simplified ASCII tree structure of a directory", inputSchema: z.object({ path: z.string(), depth: z.number().optional().default(2), }), }; export async function fileTreeHandler(args: { path: string; depth?: number }) { try { const maxDepth = args.depth || 2; let tree = "."; async function buildTree( dir: string, currentDepth: number, prefix: string, ) { if (currentDepth > maxDepth) return; const entries = await fs.promises.readdir(dir, { withFileTypes: true }); // Sort: folders first, then files entries.sort((a: any, b: any) => { if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }); for (let i = 0; i < entries.length; i++) { const entry = entries[i]; if (entry.name.startsWith(".") || entry.name === "node_modules") continue; const isLast = i === entries.length - 1; const connector = isLast ? "└── " : "├── "; const childPrefix = isLast ? " " : "│ "; tree += `\n${prefix}${connector}${entry.name}${entry.isDirectory() ? "/" : ""}`; if (entry.isDirectory()) { await buildTree( path.join(dir, entry.name), currentDepth + 1, prefix + childPrefix, ); } } } await buildTree(args.path, 1, ""); return { content: [{ type: "text", text: `\`\`\`\n${tree}\n\`\`\`` }], }; } catch (error) { return { content: [ { type: "text", text: `Error generating tree: ${(error as Error).message}`, }, ], isError: true, }; } } export const filesystemTools = { listFilesSchema, listFilesHandler, readFileSnippetSchema, readFileSnippetHandler, searchFilesSchema, searchFilesHandler, fileTreeSchema, fileTreeHandler, };

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/millsydotdev/Code-MCP'

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