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
};