diff --git a/src/filesystem/README.md b/src/filesystem/README.md index ac63f39a5f..57784a9367 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -130,6 +130,14 @@ The server's directory access control follows this flow: - `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 6723f43600..120625b81c 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -169,6 +169,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(), @@ -548,6 +553,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: @@ -944,6 +956,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: [{