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