download_file
Retrieve analysis results from the REMnux MCP Server by downloading files from the output directory. Files are automatically protected in password-secured archives to prevent security system triggers during transfer.
Instructions
Download a file from the output directory (returns base64-encoded content). Use this to retrieve analysis results. Files are wrapped in a password-protected archive by default to prevent AV/EDR triggers. Pass archive: false for harmless files like text reports. Provide output_path to save directly to the host filesystem.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| file_path | Yes | File path relative to the output directory | |
| output_path | Yes | Directory on host to save the downloaded file | |
| archive | No | Wrap the file in a password-protected archive before transfer (default: true). Protects against AV/EDR triggers on the host. Pass false for harmless files like text reports. |
Implementation Reference
- src/handlers/download-file.ts:37-177 (handler)Main handler function `handleDownloadFile` that implements the download_file tool. It validates file paths, checks file size (200MB limit), optionally creates password-protected archives (zip/7z/rar), transfers files from REMnux to the host filesystem, and returns formatted responses with file metadata (path, size, SHA256, archive info).export async function handleDownloadFile( deps: HandlerDeps, args: DownloadFileArgs ) { const startTime = Date.now(); const { connector, config } = deps; const shouldArchive = args.archive !== false; // Validate file path (skip unless --sandbox) if (!config.noSandbox) { const validation = validateFilePath(args.file_path, config.outputDir); if (!validation.safe) { return formatError("download_file", new REMnuxError( validation.error || "Invalid file path", "INVALID_PATH", "validation", "Use a relative path within the output directory", ), startTime); } } // Validate outputPath const pathValidation = validateHostPath(args.output_path); if (!pathValidation.valid) { return formatError("download_file", new REMnuxError( pathValidation.error || "Invalid output path", "INVALID_PATH", "validation", "Provide an absolute path to a directory on the host filesystem", ), startTime); } // Verify output directory exists and is a directory if (!existsSync(args.output_path) || !statSync(args.output_path).isDirectory()) { return formatError("download_file", new REMnuxError( `Output path does not exist or is not a directory: ${args.output_path}`, "INVALID_PATH", "validation", "Provide an absolute path to an existing directory", ), startTime); } const fullPath = `${config.outputDir}/${args.file_path}`; try { // Get file size and hash (separate calls to avoid shell interpolation) const statResult = await connector.execute( ["stat", "-c", "%s", fullPath], { timeout: 30000 } ); const hashResult = await connector.execute( ["sha256sum", fullPath], { timeout: 30000 } ); const sizeBytes = parseInt((statResult.stdout || "0").trim(), 10); // Guard against oversized downloads if (sizeBytes > MAX_DOWNLOAD_SIZE) { return formatError("download_file", new REMnuxError( `File exceeds ${MAX_DOWNLOAD_SIZE / 1024 / 1024}MB download limit (got ${(sizeBytes / 1024 / 1024).toFixed(2)}MB)`, "FILE_TOO_LARGE", "validation", "Use run_tool with 'split' to break the file into smaller parts first", ), startTime); } const sha256 = (hashResult.stdout || "").trim().split(/\s+/)[0] || "unknown"; const filename = basename(args.file_path); if (shouldArchive) { // Determine archive format and password from session state const archiveMeta = deps.sessionState.getArchiveInfo(filename); const archiveFormat = archiveMeta?.format ?? DEFAULT_ARCHIVE_FORMAT; const archivePassword = archiveMeta?.password ?? DEFAULT_ARCHIVE_PASSWORD; // Defense-in-depth: reject passwords with shell metacharacters if (/[;&|`$\n\r'"\\]/.test(archivePassword)) { return formatError("download_file", new REMnuxError( "Archive password contains unsafe characters", "INVALID_PASSWORD", "validation", "Try downloading with archive: false", ), startTime); } // Create temp archive path inside REMnux const timestamp = Date.now(); const archiveName = `${filename}${archiveExtension(archiveFormat)}`; const remoteTmpArchive = `/tmp/dl_${timestamp}_${archiveName}`; // Create password-protected archive const archiveCmd = getArchiveCommand(archiveFormat, remoteTmpArchive, fullPath, archivePassword); const archiveResult = await connector.execute(archiveCmd, { timeout: Math.max(60000, sizeBytes / 1024), }); if (archiveResult.exitCode !== 0) { return formatError("download_file", new REMnuxError( `Failed to create archive: ${archiveResult.stderr || archiveResult.stdout}`, "ARCHIVE_FAILED", "tool_failure", "Try downloading with archive: false", ), startTime); } // Transfer archive to host const hostPath = join(args.output_path, archiveName); try { await connector.readFileToPath(remoteTmpArchive, hostPath); } finally { // Clean up temp archive inside REMnux await connector.execute(["rm", "-f", remoteTmpArchive], { timeout: 10000 }).catch(() => {}); } return formatResponse("download_file", { file_path: args.file_path, size_bytes: sizeBytes, sha256, host_path: hostPath, archived: true, archive_format: archiveFormat, archive_password: archivePassword, }, startTime); } // No archiving — transfer raw file const hostPath = join(args.output_path, filename); await connector.readFileToPath(fullPath, hostPath); return formatResponse("download_file", { file_path: args.file_path, size_bytes: sizeBytes, sha256, host_path: hostPath, archived: false, }, startTime); } catch (error) { return formatError("download_file", toREMnuxError(error, deps.config.mode), startTime); } }
- src/schemas/tools.ts:34-42 (schema)Zod schema `downloadFileSchema` defining the tool's input parameters: file_path (relative path in output directory), output_path (host directory to save to), and archive (optional boolean, default true) to control password-protected archive wrapping. Also exports the TypeScript type `DownloadFileArgs`.export const downloadFileSchema = z.object({ file_path: z.string().describe("File path relative to the output directory"), output_path: z.string().describe("Directory on host to save the downloaded file"), archive: z.boolean().optional().default(true).describe( "Wrap the file in a password-protected archive before transfer (default: true). " + "Protects against AV/EDR triggers on the host. Pass false for harmless files like text reports." ), }); export type DownloadFileArgs = z.input<typeof downloadFileSchema>;
- src/index.ts:168-177 (registration)Tool registration where `download_file` is registered with the MCP server using `server.tool()`. Includes tool description explaining archiving behavior to prevent AV/EDR triggers, and wires up the schema and handler function.// Tool: download_file - Download a file from the output directory server.tool( "download_file", "Download a file from the output directory (returns base64-encoded content). Use this to retrieve analysis results. " + "Files are wrapped in a password-protected archive by default to prevent AV/EDR triggers. " + "Pass archive: false for harmless files like text reports. " + "Provide output_path to save directly to the host filesystem.", downloadFileSchema.shape, (args) => handleDownloadFile(deps, args) );
- src/handlers/download-file.ts:17-35 (helper)Helper functions for archive creation: `getArchiveCommand()` builds command-line arguments for zip/7z/rar formats with password protection, and `archiveExtension()` returns the appropriate file extension (.zip, .7z, .rar) for each format.function getArchiveCommand( format: "zip" | "7z" | "rar", archivePath: string, sourcePath: string, password: string ): string[] { switch (format) { case "zip": return ["zip", "-j", "-P", password, archivePath, sourcePath]; case "7z": return ["7z", "a", `-p${password}`, "-mhe=on", archivePath, sourcePath]; case "rar": return ["rar", "a", `-p${password}`, "-hp", archivePath, sourcePath]; } } function archiveExtension(format: "zip" | "7z" | "rar"): string { return `.${format}`; }