/ src / tools / filesystem-tools.ts
filesystem-tools.ts
   1  import fs from "fs/promises";
   2  
   3  import path from "path";
   4  
   5  import { minimatch } from "minimatch";
   6  
   7  import { zodToJsonSchema } from "zod-to-json-schema";
   8  
   9  import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
  10  
  11  import { expandHome, normalizePath } from "../utils/path-utils.js";
  12  
  13  import { isPathWithinAllowedDirectories } from "../utils/path-validation.js";
  14  
  15  import {
  16    MakeDirectoryArgsSchema,
  17    ListDirectoryArgsSchema,
  18    ListDirectoryWithSizesArgsSchema,
  19    DirectoryTreeArgsSchema,
  20    MoveFileArgsSchema,
  21    GetFileInfoArgsSchema,
  22    RegisterDirectoryArgsSchema,
  23    FileOperationsArgsSchema,
  24    DeleteFilesArgsSchema,
  25    type MakeDirectoryArgs,
  26    type ListDirectoryArgs,
  27    type ListDirectoryWithSizesArgs,
  28    type DirectoryTreeArgs,
  29    type MoveFileArgs,
  30    type GetFileInfoArgs,
  31    type RegisterDirectoryArgs,
  32    type FileOperationsArgs,
  33    type DeleteFilesArgs,
  34  } from "../types/index.js";
  35  
  36  import {
  37    validatePath,
  38    getFileStats,
  39    formatSize,
  40    getAllowedDirectories,
  41    setAllowedDirectories,
  42    shouldIgnoreFolder,
  43    getIgnoredFolders,
  44  } from "../utils/lib.js";
  45  import {
  46    createEmptyObjectSchema,
  47    createPathArraySchema,
  48    sanitizeToolInputSchema,
  49  } from "../utils/tool-schema.js";
  50  
  51  const ToolInputSchema = ToolSchema.shape.inputSchema;
  52  
  53  type ToolInput = any;
  54  
  55  // Internal interfaces for unified list_directory implementation
  56  interface FileEntry {
  57    name: string;
  58    path: string;
  59    isDirectory: boolean;
  60    size: number;
  61    modifiedTime: Date;
  62    children?: FileEntry[];
  63  }
  64  
  65  interface ListingResult {
  66    entries: FileEntry[];
  67    excludedByPatterns: number;
  68    excludedByIgnoreRules: number;
  69  }
  70  
  71  export function getFileSystemTools() {
  72    // Get current allowed directories for dynamic descriptions
  73    const currentAllowedDirs = getAllowedDirectories();
  74  
  75    // Generate dynamic text for pre-approved directories
  76    const generateApprovedDirsText = (): string => {
  77      if (currentAllowedDirs.length === 0) {
  78        return "\n\nCURRENTLY ACCESSIBLE DIRECTORIES: None. Use this tool to register directories for access.";
  79      }
  80  
  81      const dirList = currentAllowedDirs.map((dir) => `  - ${dir}`).join("\n");
  82      return `\n\nPRE-APPROVED DIRECTORIES (already accessible, DO NOT register these):\n${dirList}\n\nIMPORTANT: These directories and their subdirectories are ALREADY accessible to all filesystem tools. Do NOT use register_directory for these paths or any subdirectories within them.`;
  83    };
  84  
  85    return [
  86      {
  87        name: "make_directory",
  88        description:
  89          "Create single or multiple directories with recursive parent creation " +
  90          "(like Unix 'mkdir -p'). Idempotent - won't error if directories exist. " +
  91          "Only works within allowed directories.",
  92        inputSchema: {
  93          type: "object",
  94          properties: {
  95            paths: createPathArraySchema(
  96              "Directory paths to create. For maximum MCP client compatibility, provide an array even when creating a single directory."
  97            ),
  98          },
  99          required: ["paths"],
 100          additionalProperties: false,
 101        } as ToolInput,
 102      },
 103      {
 104        name: "list_directory",
 105        description:
 106          "List directory contents with flexible output formats. Replaces the previous " +
 107          "list_directory, list_directory_with_sizes, and directory_tree tools. " +
 108          "Supports simple listings, detailed views with sizes/timestamps, hierarchical " +
 109          "tree display, and structured JSON output. Automatically filters globally " +
 110          "configured ignored folders. Only works within allowed directories.",
 111        inputSchema: sanitizeToolInputSchema(
 112          zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput
 113        ),
 114      },
 115      {
 116        name: "move_file",
 117        description:
 118          "Relocate or rename files and directories in a single atomic operation. " +
 119          "Supports cross-directory moves with simultaneous renaming when needed. " +
 120          "Fails safely if the destination path already exists to prevent accidental overwrites. " +
 121          "Can also perform simple same-directory renames. " +
 122          "Both source and destination must be within allowed directories.",
 123        inputSchema: sanitizeToolInputSchema(
 124          zodToJsonSchema(MoveFileArgsSchema) as ToolInput
 125        ),
 126      },
 127      {
 128        name: "get_file_info",
 129        description:
 130          "Extract comprehensive metadata and statistics for files or directories. " +
 131          "Provides detailed information including size, timestamps (creation and last modification), permissions, and entry type. " +
 132          "Perfect for inspecting file properties and attributes without accessing the actual content. " +
 133          "Only works within allowed directories.",
 134        inputSchema: sanitizeToolInputSchema(
 135          zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput
 136        ),
 137      },
 138      {
 139        name: "register_directory",
 140        description:
 141          "Register a directory for access. This allows the AI to dynamically gain access " +
 142          "to directories specified by the human user during conversation. The directory " +
 143          "and all its subdirectories will become accessible for all filesystem operations." +
 144          generateApprovedDirsText(),
 145        inputSchema: sanitizeToolInputSchema(
 146          zodToJsonSchema(RegisterDirectoryArgsSchema) as ToolInput
 147        ),
 148      },
 149      {
 150        name: "list_allowed_directories",
 151        description:
 152          "Display all directories currently accessible to the server. " +
 153          "Note that subdirectories within listed paths are implicitly accessible as well. " +
 154          "Use this to determine available filesystem scope and plan operations accordingly before attempting file access." +
 155          generateApprovedDirsText(),
 156        inputSchema: createEmptyObjectSchema() as ToolInput,
 157      },
 158      {
 159        name: "file_operations",
 160        description:
 161          "Perform bulk file operations (move, copy, rename) on single or multiple files and directories concurrently. " +
 162          "All operations are validated for security before execution. Supports conflict resolution " +
 163          "strategies for existing destinations. Maximum 100 files per operation for performance.",
 164        inputSchema: {
 165          type: "object",
 166          properties: {
 167            operation: {
 168              type: "string",
 169              enum: ["move", "copy", "rename"],
 170              description: "The type of file operation to perform",
 171            },
 172            files: {
 173              type: "array",
 174              items: {
 175                type: "object",
 176                properties: {
 177                  source: {
 178                    type: "string",
 179                    description: "Source file or directory path",
 180                  },
 181                  destination: {
 182                    type: "string",
 183                    description: "Destination file or directory path",
 184                  },
 185                },
 186                required: ["source", "destination"],
 187                additionalProperties: false,
 188              },
 189              minItems: 1,
 190              maxItems: 100,
 191              description: "Array of source-destination file pairs",
 192            },
 193            onConflict: {
 194              type: "string",
 195              enum: ["skip", "overwrite", "error"],
 196              description: "How to handle destination conflicts",
 197              default: "error",
 198            },
 199          },
 200          required: ["operation", "files"],
 201          additionalProperties: false,
 202        } as ToolInput,
 203      },
 204      {
 205        name: "delete_files",
 206        description:
 207          "Delete single or multiple files and directories securely. " +
 208          "Supports recursive directory deletion with safety controls. " +
 209          "All paths are validated before deletion begins. " +
 210          "Operations are processed concurrently for performance. " +
 211          "Maximum 100 paths per operation. " +
 212          "Only works within allowed directories.",
 213        inputSchema: sanitizeToolInputSchema(
 214          zodToJsonSchema(DeleteFilesArgsSchema) as ToolInput
 215        ),
 216      },
 217    ];
 218  }
 219  
 220  // ============================================================================
 221  // UNIFIED LIST_DIRECTORY IMPLEMENTATION
 222  // ============================================================================
 223  
 224  /**
 225   * Helper: Collect file entry metadata
 226   */
 227  async function collectFileEntry(
 228    entryPath: string,
 229    dirent: any,
 230  ): Promise<FileEntry> {
 231    try {
 232      const stats = await fs.stat(entryPath);
 233      return {
 234        name: dirent.name,
 235        path: entryPath,
 236        isDirectory: dirent.isDirectory(),
 237        size: stats.size,
 238        modifiedTime: stats.mtime,
 239      };
 240    } catch (error) {
 241      // Return minimal entry on error
 242      return {
 243        name: dirent.name,
 244        path: entryPath,
 245        isDirectory: dirent.isDirectory(),
 246        size: 0,
 247        modifiedTime: new Date(0),
 248      };
 249    }
 250  }
 251  
 252  async function filterAndCollectEntries(
 253    rawEntries: any[],
 254  
 255    basePath: string,
 256  
 257    args: ListDirectoryArgs,
 258  ): Promise<ListingResult> {
 259    let excludedByPatterns = 0;
 260  
 261    let excludedByIgnoreRules = 0;
 262  
 263    const entries: FileEntry[] = [];
 264  
 265    for (const dirent of rawEntries) {
 266      // Check global ignore rules
 267  
 268      if (dirent.isDirectory() && shouldIgnoreFolder(dirent.name)) {
 269        excludedByIgnoreRules++;
 270  
 271        continue;
 272      }
 273  
 274      if (args.excludePatterns && args.excludePatterns.length > 0) {
 275        const shouldExclude = args.excludePatterns.some((pattern: string) => {
 276          return minimatch(dirent.name, pattern, { dot: true });
 277        });
 278  
 279        if (shouldExclude) {
 280          excludedByPatterns++;
 281  
 282          continue;
 283        }
 284      }
 285  
 286      // Collect entry with metadata
 287  
 288      const entryPath = path.join(basePath, dirent.name);
 289  
 290      const entry = await collectFileEntry(entryPath, dirent);
 291  
 292      entries.push(entry);
 293    }
 294  
 295    return { entries, excludedByPatterns, excludedByIgnoreRules };
 296  }
 297  
 298  /**
 299   * Helper: Recursively expand directory entries for tree/json formats
 300   */
 301  async function recursivelyExpandEntries(
 302    entries: FileEntry[],
 303    args: ListDirectoryArgs,
 304  ): Promise<void> {
 305    for (const entry of entries) {
 306      if (entry.isDirectory) {
 307        try {
 308          const subEntries = await fs.readdir(entry.path, {
 309            withFileTypes: true,
 310          });
 311          const { entries: children } = await filterAndCollectEntries(
 312            subEntries,
 313            entry.path,
 314            args,
 315          );
 316          entry.children = children;
 317  
 318          // Recurse
 319          await recursivelyExpandEntries(children, args);
 320        } catch (error) {
 321          entry.children = [];
 322        }
 323      }
 324    }
 325  }
 326  
 327  /**
 328   * Helper: Sort entries (Gemini-inspired - always dirs first, then by criterion)
 329   */
 330  function sortEntries(entries: FileEntry[], sortBy: string): FileEntry[] {
 331    return [...entries].sort((a, b) => {
 332      // Always group directories first (Gemini best practice)
 333      if (a.isDirectory && !b.isDirectory) return -1;
 334      if (!a.isDirectory && b.isDirectory) return 1;
 335  
 336      // Then apply sort criterion
 337      if (sortBy === "size") {
 338        return b.size - a.size; // Descending by size
 339      }
 340  
 341      // Default: alphabetical by name
 342      return a.name.localeCompare(b.name);
 343    });
 344  }
 345  
 346  /**
 347   * Helper: Count files recursively
 348   */
 349  function countFiles(entries: FileEntry[]): number {
 350    let count = 0;
 351    for (const entry of entries) {
 352      if (!entry.isDirectory) {
 353        count++;
 354      }
 355      if (entry.children) {
 356        count += countFiles(entry.children);
 357      }
 358    }
 359    return count;
 360  }
 361  
 362  /**
 363   * Helper: Count directories recursively
 364   */
 365  function countDirectories(entries: FileEntry[]): number {
 366    let count = 0;
 367    for (const entry of entries) {
 368      if (entry.isDirectory) {
 369        count++;
 370        if (entry.children) {
 371          count += countDirectories(entry.children);
 372        }
 373      }
 374    }
 375    return count;
 376  }
 377  
 378  /**
 379   * Helper: Calculate total size recursively
 380   */
 381  function calculateTotalSize(entries: FileEntry[]): number {
 382    let total = 0;
 383    for (const entry of entries) {
 384      if (!entry.isDirectory) {
 385        total += entry.size;
 386      }
 387      if (entry.children) {
 388        total += calculateTotalSize(entry.children);
 389      }
 390    }
 391    return total;
 392  }
 393  
 394  /**
 395   * Format: Simple (default)
 396   */
 397  function formatSimple(
 398    entries: FileEntry[],
 399    excludedByPatterns: number,
 400    excludedByIgnoreRules: number,
 401  ): { content: any[] } {
 402    const lines = entries.map((entry) => {
 403      const prefix = entry.isDirectory ? "[DIR]" : "[FILE]";
 404      return `${prefix} ${entry.name}`;
 405    });
 406  
 407    // Summary
 408    const totalFiles = entries.filter((e) => !e.isDirectory).length;
 409    const totalDirs = entries.filter((e) => e.isDirectory).length;
 410    lines.push("");
 411    lines.push(`Total: ${totalFiles} files, ${totalDirs} directories`);
 412  
 413    // Show exclusion counts
 414    const totalExcluded = excludedByPatterns + excludedByIgnoreRules;
 415    if (totalExcluded > 0) {
 416      lines.push(`(${totalExcluded} filtered by ignore rules)`);
 417    }
 418  
 419    return { content: [{ type: "text", text: lines.join("\n") }] };
 420  }
 421  
 422  /**
 423   * Format: Detailed (with sizes and metadata)
 424   */
 425  function formatDetailed(
 426    entries: FileEntry[],
 427    excludedByPatterns: number,
 428    excludedByIgnoreRules: number,
 429  ): { content: any[] } {
 430    const header = "Type      Name                  Size        Modified";
 431    const separator = "-".repeat(70);
 432  
 433    const lines = entries.map((entry) => {
 434      const type = entry.isDirectory ? "[DIR]" : "[FILE]";
 435      const name = entry.name.padEnd(20);
 436      const size = entry.isDirectory
 437        ? "-".padStart(11)
 438        : formatSize(entry.size).padStart(11);
 439      const mtime = entry.modifiedTime
 440        .toISOString()
 441        .slice(0, 19)
 442        .replace("T", " ");
 443  
 444      return `${type}     ${name} ${size} ${mtime}`;
 445    });
 446  
 447    // Summary
 448    const totalFiles = entries.filter((e) => !e.isDirectory).length;
 449    const totalDirs = entries.filter((e) => e.isDirectory).length;
 450    const totalSize = entries.reduce(
 451      (sum, e) => sum + (e.isDirectory ? 0 : e.size),
 452      0,
 453    );
 454  
 455    const output = [
 456      header,
 457      separator,
 458      ...lines,
 459      "",
 460      `Total: ${totalFiles} files, ${totalDirs} directories`,
 461      `Combined size: ${formatSize(totalSize)}`,
 462    ];
 463  
 464    const totalExcluded = excludedByPatterns + excludedByIgnoreRules;
 465    if (totalExcluded > 0) {
 466      output.push(`(${totalExcluded} filtered by ignore rules)`);
 467    }
 468  
 469    return { content: [{ type: "text", text: output.join("\n") }] };
 470  }
 471  
 472  /**
 473   * Format: Tree (hierarchical text tree)
 474   */
 475  function formatTree(
 476    entries: FileEntry[],
 477    excludedByPatterns: number,
 478    prefix: string = "",
 479    isRoot: boolean = true,
 480  ): { content: any[] } {
 481    const lines: string[] = [];
 482  
 483    if (isRoot) {
 484      lines.push(".");
 485    }
 486  
 487    entries.forEach((entry, index) => {
 488      const isLast = index === entries.length - 1;
 489      const connector = isLast ? "└── " : "├── ";
 490      const suffix = entry.isDirectory ? "/" : "";
 491  
 492      lines.push(`${prefix}${connector}${entry.name}${suffix}`);
 493  
 494      if (entry.children && entry.children.length > 0) {
 495        const childPrefix = prefix + (isLast ? "    " : "│   ");
 496        const childResult = formatTree(entry.children, 0, childPrefix, false);
 497        lines.push(
 498          ...childResult.content[0].text.split("\n").filter((l: string) => l),
 499        );
 500      }
 501    });
 502  
 503    if (isRoot) {
 504      const totalFiles = countFiles(entries);
 505      const totalDirs = countDirectories(entries);
 506      lines.push("");
 507      lines.push(`${totalDirs} directories, ${totalFiles} files`);
 508  
 509      if (excludedByPatterns > 0) {
 510        lines.push(`(${excludedByPatterns} entries excluded by patterns)`);
 511      }
 512    }
 513  
 514    return { content: [{ type: "text", text: lines.join("\n") }] };
 515  }
 516  
 517  /**
 518   * Format: JSON (structured data)
 519   */
 520  function formatJson(
 521    entries: FileEntry[],
 522    basePath: string,
 523    excludedByPatterns: number,
 524    excludedByIgnoreRules: number,
 525  ): { content: any[] } {
 526    const totalFiles = countFiles(entries);
 527    const totalDirs = countDirectories(entries);
 528    const totalSize = calculateTotalSize(entries);
 529  
 530    const output = {
 531      path: basePath,
 532      entries: entries.map((e) => ({
 533        name: e.name,
 534        type: e.isDirectory ? "directory" : "file",
 535        path: e.path,
 536        isDirectory: e.isDirectory,
 537        size: e.size,
 538        modifiedTime: e.modifiedTime.toISOString(),
 539        ...(e.children && { children: e.children }),
 540      })),
 541      summary: {
 542        totalFiles,
 543        totalDirectories: totalDirs,
 544        totalSize,
 545        totalSizeFormatted: formatSize(totalSize),
 546        excludedByPatterns,
 547        excludedByIgnoreRules,
 548      },
 549    };
 550  
 551    return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
 552  }
 553  
 554  /**
 555   * Format output based on selected format
 556   */
 557  function formatOutput(
 558    entries: FileEntry[],
 559    args: ListDirectoryArgs,
 560    excludedByPatterns: number,
 561    excludedByIgnoreRules: number,
 562  ): { content: any[] } {
 563    switch (args.format) {
 564      case "simple":
 565        return formatSimple(entries, excludedByPatterns, excludedByIgnoreRules);
 566      case "detailed":
 567        return formatDetailed(entries, excludedByPatterns, excludedByIgnoreRules);
 568      case "tree":
 569        return formatTree(entries, excludedByPatterns);
 570      case "json":
 571        return formatJson(
 572          entries,
 573          args.path,
 574          excludedByPatterns,
 575          excludedByIgnoreRules,
 576        );
 577      default:
 578        return formatSimple(entries, excludedByPatterns, excludedByIgnoreRules);
 579    }
 580  }
 581  
 582  /**
 583   * Main unified list_directory implementation
 584   */
 585  async function listDirectory(
 586    args: ListDirectoryArgs,
 587  ): Promise<{ content: any[] }> {
 588    // Step 1: Validate path
 589    const validPath = await validatePath(args.path);
 590  
 591    // Step 2: Read directory
 592    const rawEntries = await fs.readdir(validPath, { withFileTypes: true });
 593  
 594    // Step 3: Apply filtering and collect metadata
 595    const { entries, excludedByPatterns, excludedByIgnoreRules } =
 596      await filterAndCollectEntries(rawEntries, validPath, args);
 597  
 598    // Step 4: Handle recursive formats (tree and json)
 599    if (args.format === "tree" || args.format === "json") {
 600      await recursivelyExpandEntries(entries, args);
 601    }
 602  
 603    // Step 5: Apply sorting (always dirs first, then by sortBy)
 604    const sorted = sortEntries(entries, args.sortBy || "name");
 605  
 606    // Step 6: Format output
 607    return formatOutput(sorted, args, excludedByPatterns, excludedByIgnoreRules);
 608  }
 609  
 610  // ============================================================================
 611  // END UNIFIED LIST_DIRECTORY IMPLEMENTATION
 612  // ============================================================================
 613  
 614  export async function handleFileSystemTool(name: string, args: any) {
 615    switch (name) {
 616      case "make_directory": {
 617        const parsed = MakeDirectoryArgsSchema.safeParse(args);
 618        if (!parsed.success) {
 619          throw new Error(
 620            `Invalid arguments for make_directory: ${parsed.error}`,
 621          );
 622        }
 623  
 624        // Defensive handling for MCP clients that may stringify arrays
 625        // Some MCP clients (e.g., Claude Desktop) incorrectly serialize array parameters
 626        // as stringified JSON instead of proper arrays. This workaround detects and fixes that.
 627        let pathsInput = parsed.data.paths;
 628  
 629        // If paths is a string that looks like a JSON array, try to parse it
 630        if (typeof pathsInput === "string" && pathsInput.trim().startsWith("[")) {
 631          try {
 632            const parsedArray = JSON.parse(pathsInput);
 633            if (Array.isArray(parsedArray)) {
 634              pathsInput = parsedArray;
 635              // Log for diagnostics - helps identify which clients have serialization issues
 636              console.error(
 637                "[INFO] make_directory: Detected and corrected stringified array parameter",
 638              );
 639            }
 640          } catch {
 641            // If parsing fails, treat as single path (existing behavior)
 642            // This handles edge cases like paths literally named "[something]"
 643          }
 644        }
 645  
 646        // Normalize to array (single path or multiple paths)
 647        const pathsToCreate = Array.isArray(pathsInput)
 648          ? pathsInput
 649          : [pathsInput];
 650  
 651        // Validate all paths first (atomic - fail before any creation)
 652        const allowedDirs = getAllowedDirectories();
 653        const validatedPaths = pathsToCreate.map((dirPath) => {
 654          const expandedPath = expandHome(dirPath);
 655          // Resolve to absolute path - required by isPathWithinAllowedDirectories
 656          const absolutePath = path.isAbsolute(expandedPath)
 657            ? path.resolve(expandedPath)
 658            : path.resolve(process.cwd(), expandedPath);
 659          const normalized = normalizePath(absolutePath);
 660  
 661          // Use secure path validation function to prevent prefix collision attacks
 662          // (CVE-2025-54794 pattern: ensures path separator is required, not just prefix match)
 663          if (!isPathWithinAllowedDirectories(normalized, allowedDirs)) {
 664            throw new Error(
 665              `Access denied: Path ${dirPath} is not within allowed directories`,
 666            );
 667          }
 668  
 669          return { original: dirPath, normalized };
 670        });
 671  
 672        // All validated - now create them concurrently
 673        const results = await Promise.all(
 674          validatedPaths.map(async ({ original, normalized }) => {
 675            await fs.mkdir(normalized, { recursive: true });
 676            return original;
 677          }),
 678        );
 679  
 680        // Format response based on single vs batch
 681        const message =
 682          results.length === 1
 683            ? `Successfully created directory ${results[0]}`
 684            : `Successfully created ${results.length} directories:\n${results
 685                .map((p) => `  - ${p}`)
 686                .join("\n")}`;
 687  
 688        return {
 689          content: [
 690            {
 691              type: "text",
 692              text: message,
 693            },
 694          ],
 695        };
 696      }
 697  
 698      case "list_directory": {
 699        const parsed = ListDirectoryArgsSchema.safeParse(args);
 700        if (!parsed.success) {
 701          throw new Error(
 702            `Invalid arguments for list_directory: ${parsed.error}`,
 703          );
 704        }
 705        return await listDirectory(parsed.data);
 706      }
 707  
 708      case "move_file": {
 709        const parsed = MoveFileArgsSchema.safeParse(args);
 710        if (!parsed.success) {
 711          throw new Error(`Invalid arguments for move_file: ${parsed.error}`);
 712        }
 713        const validSourcePath = await validatePath(parsed.data.source);
 714        const validDestPath = await validatePath(parsed.data.destination);
 715        await fs.rename(validSourcePath, validDestPath);
 716        return {
 717          content: [
 718            {
 719              type: "text",
 720              text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}`,
 721            },
 722          ],
 723        };
 724      }
 725  
 726      case "get_file_info": {
 727        const parsed = GetFileInfoArgsSchema.safeParse(args);
 728        if (!parsed.success) {
 729          throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`);
 730        }
 731        const validPath = await validatePath(parsed.data.path);
 732        const info = await getFileStats(validPath);
 733        return {
 734          content: [
 735            {
 736              type: "text",
 737              text: Object.entries(info)
 738                .map(([key, value]) => `${key}: ${value}`)
 739                .join("\n"),
 740            },
 741          ],
 742        };
 743      }
 744  
 745      case "register_directory": {
 746        const parsed = RegisterDirectoryArgsSchema.safeParse(args);
 747        if (!parsed.success) {
 748          throw new Error(
 749            `Invalid arguments for register_directory: ${parsed.error}`,
 750          );
 751        }
 752  
 753        const expandedPath = expandHome(parsed.data.path);
 754        const absolutePath = path.resolve(expandedPath);
 755        const normalizedPath = normalizePath(absolutePath);
 756  
 757        // Validate that the path exists and is a directory
 758        try {
 759          const stats = await fs.stat(absolutePath);
 760          if (!stats.isDirectory()) {
 761            throw new Error(`Path ${absolutePath} is not a directory`);
 762          }
 763        } catch (error) {
 764          if ((error as NodeJS.ErrnoException).code === "ENOENT") {
 765            throw new Error(`Directory ${absolutePath} does not exist`);
 766          }
 767          throw error;
 768        }
 769  
 770        // Add to allowed directories
 771        const currentDirs = getAllowedDirectories();
 772        if (!currentDirs.includes(normalizedPath)) {
 773          setAllowedDirectories([...currentDirs, normalizedPath]);
 774          return {
 775            content: [
 776              {
 777                type: "text",
 778                text: `Successfully registered directory: ${parsed.data.path} (${normalizedPath})`,
 779              },
 780            ],
 781          };
 782        } else {
 783          return {
 784            content: [
 785              {
 786                type: "text",
 787                text: `Directory already registered: ${parsed.data.path} (${normalizedPath})`,
 788              },
 789            ],
 790          };
 791        }
 792      }
 793  
 794      case "list_allowed_directories": {
 795        return {
 796          content: [
 797            {
 798              type: "text",
 799  
 800              text: `Allowed directories:\n${getAllowedDirectories().join("\n")}`,
 801            },
 802          ],
 803        };
 804      }
 805  
 806      case "file_operations": {
 807        const parsed = FileOperationsArgsSchema.safeParse(args);
 808  
 809        if (!parsed.success) {
 810          throw new Error(
 811            `Invalid arguments for file_operations: ${parsed.error}`,
 812          );
 813        }
 814  
 815        // Phase 1: Path Validation
 816        const validationPromises = parsed.data.files.map(
 817          async (file: FileOperationsArgs["files"][number], index: number) => {
 818            try {
 819              const validSource = await validatePath(file.source);
 820  
 821              const validDest = await validatePath(file.destination);
 822  
 823              return {
 824                index,
 825  
 826                source: file.source,
 827  
 828                destination: file.destination,
 829  
 830                validSource,
 831  
 832                validDest,
 833  
 834                success: true,
 835              };
 836            } catch (error) {
 837              return {
 838                index,
 839                source: file.source,
 840  
 841                destination: file.destination,
 842  
 843                success: false,
 844  
 845                error: error instanceof Error ? error.message : String(error),
 846              };
 847            }
 848          },
 849        );
 850  
 851        const validatedFiles = await Promise.all(validationPromises);
 852  
 853        // Check for validation errors
 854  
 855        const validationErrors = validatedFiles.filter((f) => !f.success);
 856  
 857        if (validationErrors.length > 0) {
 858          const errorMessages = validationErrors
 859  
 860            .map(
 861              (f) =>
 862                `${f.source} → ${f.destination}: ${f.error || "Unknown error"}`,
 863            )
 864  
 865            .join("\n");
 866  
 867          throw new Error(`Path validation failed:\n${errorMessages}`);
 868        }
 869  
 870        // Phase 2: Conflict Detection
 871  
 872        const conflictChecks = await Promise.all(
 873          validatedFiles.map(async (file) => {
 874            try {
 875              await fs.access(file.validDest!);
 876  
 877              return {
 878                ...file,
 879  
 880                hasConflict: true,
 881              };
 882            } catch {
 883              return {
 884                ...file,
 885  
 886                hasConflict: false,
 887              };
 888            }
 889          }),
 890        );
 891  
 892        // Handle conflicts based on strategy
 893  
 894        const filesToProcess = conflictChecks.filter((file) => {
 895          if (file.hasConflict) {
 896            switch (parsed.data.onConflict) {
 897              case "skip":
 898                return false;
 899  
 900              case "error":
 901                throw new Error(
 902                  `Destination already exists: ${file.destination}`,
 903                );
 904  
 905              case "overwrite":
 906                return true;
 907            }
 908          }
 909  
 910          return true;
 911        });
 912  
 913        // Phase 3: Execute Operations
 914  
 915        const operationPromises = filesToProcess.map(async (file) => {
 916          try {
 917            switch (parsed.data.operation) {
 918              case "move":
 919  
 920              case "rename":
 921                await fs.rename(file.validSource!, file.validDest!);
 922  
 923                break;
 924  
 925              case "copy":
 926                const stats = await fs.stat(file.validSource!);
 927  
 928                if (stats.isDirectory()) {
 929                  await copyDirectoryRecursive(
 930                    file.validSource!,
 931  
 932                    file.validDest!,
 933                  );
 934                } else {
 935                  await fs.copyFile(file.validSource!, file.validDest!);
 936                }
 937                break;
 938            }
 939            return {
 940              index: file.index,
 941  
 942              source: file.source,
 943  
 944              destination: file.destination,
 945  
 946              success: true,
 947  
 948              operation: parsed.data.operation,
 949            };
 950          } catch (error) {
 951            return {
 952              index: file.index,
 953  
 954              source: file.source,
 955  
 956              destination: file.destination,
 957  
 958              success: false,
 959  
 960              error: error instanceof Error ? error.message : String(error),
 961  
 962              operation: parsed.data.operation,
 963            };
 964          }
 965        });
 966  
 967        const results = await Promise.allSettled(operationPromises);
 968  
 969        const processedResults = results.map((result, index) => {
 970          if (result.status === "fulfilled") {
 971            return result.value;
 972          } else {
 973            return {
 974              index,
 975  
 976              source: filesToProcess[index].source,
 977  
 978              destination: filesToProcess[index].destination,
 979  
 980              success: false,
 981  
 982              error:
 983                result.reason instanceof Error
 984                  ? result.reason.message
 985                  : String(result.reason),
 986  
 987              operation: parsed.data.operation,
 988            };
 989          }
 990        });
 991  
 992        // Prepare response
 993  
 994        const successful = processedResults.filter((r) => r.success);
 995  
 996        const failed = processedResults.filter((r) => !r.success);
 997  
 998        const successDetails = successful
 999  
1000          .map((r) => `✓ ${r.source} → ${r.destination}`)
1001  
1002          .join("\n");
1003  
1004        const failureDetails =
1005          failed.length > 0
1006            ? failed
1007  
1008                .map((r) => `✗ ${r.source} → ${r.destination}: ${r.error}`)
1009  
1010                .join("\n")
1011            : "";
1012  
1013        return {
1014          content: [
1015            {
1016              type: "text",
1017  
1018              text:
1019                `Successfully performed ${parsed.data.operation} operations:\n\n` +
1020                `Total operations: ${processedResults.length}\n` +
1021                `Successful: ${successful.length}\n` +
1022                `Failed: ${failed.length}\n\n` +
1023                (failed.length > 0
1024                  ? `Failed operations:\n${failureDetails}\n\n`
1025                  : "") +
1026                `Processed files:\n${successDetails}`,
1027            },
1028          ],
1029        };
1030      }
1031  
1032      case "delete_files": {
1033        const parsed = DeleteFilesArgsSchema.safeParse(args);
1034        if (!parsed.success) {
1035          throw new Error(`Invalid arguments for delete_files: ${parsed.error}`);
1036        }
1037  
1038        // Phase 1: Path Validation
1039        const validationPromises = parsed.data.paths.map(
1040          async (filePath, index) => {
1041            try {
1042              const validPath = await validatePath(filePath);
1043              return {
1044                index,
1045                originalPath: filePath,
1046                validPath,
1047                success: true,
1048              };
1049            } catch (error) {
1050              return {
1051                index,
1052                originalPath: filePath,
1053                success: false,
1054                error: error instanceof Error ? error.message : String(error),
1055              };
1056            }
1057          },
1058        );
1059  
1060        const validatedPaths = await Promise.all(validationPromises);
1061  
1062        // Check for validation errors
1063        const validationErrors = validatedPaths.filter((p) => !p.success);
1064        if (validationErrors.length > 0) {
1065          const errorMessages = validationErrors
1066            .map((p) => `${p.originalPath}: ${p.error || "Unknown error"}`)
1067            .join("\n");
1068          throw new Error(`Path validation failed:\n${errorMessages}`);
1069        }
1070  
1071        // Phase 2: Pre-deletion Checks
1072        const preCheckPromises = validatedPaths.map(async (item) => {
1073          try {
1074            const stats = await fs.stat(item.validPath!);
1075            return {
1076              ...item,
1077              exists: true,
1078              isDirectory: stats.isDirectory(),
1079            };
1080          } catch (error) {
1081            return {
1082              ...item,
1083              exists: false,
1084              isDirectory: false,
1085              error: `File does not exist: ${item.originalPath}`,
1086            };
1087          }
1088        });
1089  
1090        const checkedPaths = await Promise.all(preCheckPromises);
1091  
1092        // Filter out non-existent paths
1093        const pathsToDelete = checkedPaths.filter((p) => p.exists);
1094  
1095        if (pathsToDelete.length === 0) {
1096          throw new Error(
1097            "No valid paths to delete - all paths either don't exist or failed validation",
1098          );
1099        }
1100  
1101        // Phase 3: Execute Deletions
1102        const deletionPromises = pathsToDelete.map(async (item) => {
1103          try {
1104            if (item.isDirectory) {
1105              if (parsed.data.recursive) {
1106                // Recursive directory deletion
1107                await fs.rm(item.validPath!, {
1108                  recursive: true,
1109                  force: parsed.data.force,
1110                });
1111              } else {
1112                // Non-recursive - only delete empty directories
1113                await fs.rmdir(item.validPath!);
1114              }
1115            } else {
1116              // File deletion
1117              await fs.unlink(item.validPath!);
1118            }
1119            return {
1120              index: item.index,
1121              path: item.originalPath,
1122              success: true,
1123              isDirectory: item.isDirectory,
1124            };
1125          } catch (error) {
1126            const errorMessage =
1127              error instanceof Error ? error.message : String(error);
1128            // Provide helpful error messages
1129            let friendlyError = errorMessage;
1130            if (
1131              errorMessage.includes("ENOTEMPTY") ||
1132              errorMessage.includes("directory not empty")
1133            ) {
1134              friendlyError = `Directory not empty. Use recursive: true to delete non-empty directories.`;
1135            } else if (
1136              errorMessage.includes("EACCES") ||
1137              errorMessage.includes("EPERM")
1138            ) {
1139              friendlyError = `Permission denied. ${
1140                parsed.data.force
1141                  ? "Insufficient permissions even with force enabled."
1142                  : "Try using force: true if appropriate."
1143              }`;
1144            }
1145  
1146            return {
1147              index: item.index,
1148              path: item.originalPath,
1149              success: false,
1150              error: friendlyError,
1151              isDirectory: item.isDirectory || false,
1152            };
1153          }
1154        });
1155  
1156        const results = await Promise.allSettled(deletionPromises);
1157  
1158        // Process results
1159        const processedResults = results.map((result, index) => {
1160          if (result.status === "fulfilled") {
1161            return result.value;
1162          } else {
1163            return {
1164              index,
1165              path: pathsToDelete[index].originalPath,
1166              success: false,
1167              isDirectory: pathsToDelete[index].isDirectory || false,
1168              error:
1169                result.reason instanceof Error
1170                  ? result.reason.message
1171                  : String(result.reason),
1172            };
1173          }
1174        });
1175  
1176        // Prepare response
1177        const successful = processedResults.filter((r) => r.success);
1178        const failed = processedResults.filter((r) => !r.success);
1179  
1180        const successDetails = successful
1181          .map((r) => `✓ ${r.path}${r.isDirectory ? " (directory)" : ""}`)
1182          .join("\n");
1183  
1184        const failureDetails =
1185          failed.length > 0
1186            ? failed.map((r) => `✗ ${r.path}: ${r.error}`).join("\n")
1187            : "";
1188  
1189        // Build response message
1190        const responseLines = [
1191          `Successfully deleted ${successful.length} of ${processedResults.length} paths:`,
1192          "",
1193          `Total paths: ${processedResults.length}`,
1194          `Successful: ${successful.length}`,
1195          `Failed: ${failed.length}`,
1196          "",
1197        ];
1198  
1199        if (failed.length > 0) {
1200          responseLines.push(`Failed deletions:`, failureDetails, "");
1201        }
1202  
1203        if (successful.length > 0) {
1204          responseLines.push(`Deleted paths:`, successDetails);
1205        }
1206  
1207        return {
1208          content: [
1209            {
1210              type: "text",
1211              text: responseLines.join("\n"),
1212            },
1213          ],
1214        };
1215      }
1216  
1217      default:
1218        throw new Error(`Unknown filesystem tool: ${name}`);
1219    }
1220  }
1221  
1222  // Helper function for recursive directory copying
1223  async function copyDirectoryRecursive(
1224    source: string,
1225    destination: string,
1226  ): Promise<void> {
1227    // Create destination directory
1228    await fs.mkdir(destination, { recursive: true });
1229  
1230    // Read source directory
1231    const entries = await fs.readdir(source, { withFileTypes: true });
1232  
1233    // Copy all entries
1234    for (const entry of entries) {
1235      const sourcePath = path.join(source, entry.name);
1236      const destPath = path.join(destination, entry.name);
1237  
1238      if (entry.isDirectory()) {
1239        await copyDirectoryRecursive(sourcePath, destPath);
1240      } else {
1241        await fs.copyFile(sourcePath, destPath);
1242      }
1243    }
1244  }