Skip to main content
Glama
filesystem.ts7.22 kB
import { z } from "zod"; import fs from 'fs/promises'; import path from 'path'; /** * File System Tools - Safe interaction with the file system */ // ============================================ // 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 = await fs.readdir(args.path, { withFileTypes: true, recursive: args.recursive }); const files = entries.map(e => ({ name: e.name, type: e.isDirectory() ? 'dir' : 'file', path: path.join(e.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 }) { try { const content = await fs.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.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()) { try { const content = await fs.readFile(fullPath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { 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.readdir(dir, { withFileTypes: true }); // Sort: folders first, then files entries.sort((a, b) => { 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