Skip to main content
Glama
REMnux

REMnux MCP Server

Official
by REMnux

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
NameRequiredDescriptionDefault
file_pathYesFile path relative to the output directory
output_pathYesDirectory on host to save the downloaded file
archiveNoWrap 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

  • 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);
      }
    }
  • 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)
    );
  • 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}`;
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/REMnux/remnux-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server