From 81cf93a9039861ff45e3c66c47b0bfd0cab233a1 Mon Sep 17 00:00:00 2001 From: Nandha Reddy Date: Wed, 4 Jun 2025 19:55:07 +1000 Subject: [PATCH] feat(filesystem): add copy_file tool for efficient file/directory copying Implements a new copy_file tool that addresses issue #1581 by providing an efficient way to copy files and directories without requiring read/write operations for large files. ## Changes: - Add copy_file tool with support for both files and directories - Use fs.copyFile for files (optimized for large files) - Use fs.cp with recursive option for directories - Include proper validation and error handling - Update README documentation ## Implementation Details: - Validates both source and destination paths are within allowed directories - Checks if destination exists and fails appropriately - Follows existing tool patterns for consistency - Maintains security checks and path validation Fixes #1581 --- src/filesystem/README.md | 8 +++++++ src/filesystem/index.ts | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/filesystem/README.md b/src/filesystem/README.md index d1621d1ef3..0e93f81bbc 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -71,6 +71,14 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio - `destination` (string) - Fails if destination exists +- **copy_file** + - Copy a file or directory from source to destination + - Inputs: + - `source` (string) + - `destination` (string) + - For directories, copies all contents recursively + - Fails if destination exists + - **search_files** - Recursively search for files/directories - Inputs: diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 00b8782f11..f9ea6f12cd 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -143,6 +143,11 @@ const MoveFileArgsSchema = z.object({ destination: z.string(), }); +const CopyFileArgsSchema = z.object({ + source: z.string(), + destination: z.string(), +}); + const SearchFilesArgsSchema = z.object({ path: z.string(), pattern: z.string(), @@ -479,6 +484,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { "Only works within allowed directories.", inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput, }, + { + name: "copy_file", + description: + "Copy a file or directory from source to destination. For directories, copies all contents recursively. " + + "If destination exists, the operation will fail. Both source and destination must be within allowed directories.", + inputSchema: zodToJsonSchema(CopyFileArgsSchema) as ToolInput, + }, { name: "create_directory", description: @@ -818,6 +830,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "copy_file": { + const parsed = CopyFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for copy_file: ${parsed.error}`); + } + const validSourcePath = await validatePath(parsed.data.source); + const validDestPath = await validatePath(parsed.data.destination); + + // Check if destination already exists + try { + await fs.access(validDestPath); + throw new Error(`Destination already exists: ${parsed.data.destination}`); + } catch (error) { + // If error is NOT about file not existing, re-throw it + if (error instanceof Error && !error.message.includes('ENOENT')) { + throw error; + } + } + + // Check if source is a directory + const sourceStats = await fs.stat(validSourcePath); + + if (sourceStats.isDirectory()) { + // For directories, use fs.cp with recursive option + await fs.cp(validSourcePath, validDestPath, { recursive: true }); + } else { + // For files, use copyFile which is optimized for large files + await fs.copyFile(validSourcePath, validDestPath); + } + + return { + content: [{ type: "text", text: `Successfully copied ${parsed.data.source} to ${parsed.data.destination}` }], + }; + } + case "list_allowed_directories": { return { content: [{