download_installer
download_installer
Downloads an application installer from a HTTPS URL to a temporary directory. Validates integrity via SHA-256 checksum if provided. Use as the first step of a software reinstall workflow.
Metadata
Name
download_installer
Updated
2 months ago
Source
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/downloadInstaller.ts — download_installer skill
*
* Downloads an application installer from a HTTPS URL to a temporary
* directory. Validates integrity via SHA-256 checksum if provided.
* Use as the first step of a software reinstall workflow.
*
* Platform strategy
* -----------------
* darwin & win32 Node.js https.get() streaming to os.tmpdir()
* SHA-256 via crypto.createHash if checksumSha256 provided
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/downloadInstaller.ts
*/
import * as os from "os";
import * as nodePath from "path";
import * as fs from "fs";
import * as https from "https";
import * as crypto from "crypto";
import { z } from "zod";
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "download_installer",
description:
"Downloads an application installer from a HTTPS URL to a temporary directory. " +
"Validates integrity via SHA-256 checksum if provided. " +
"Use as the first step of a software reinstall workflow.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
schema: {
url: z
.string()
.describe("HTTPS URL to the installer (.dmg, .pkg, .exe, .msi)"),
filename: z
.string()
.optional()
.describe("Local filename. Defaults to the filename from the URL"),
checksumSha256: z
.string()
.optional()
.describe("Expected SHA-256 hash for integrity validation"),
},
} as const;
// -- Types --------------------------------------------------------------------
interface DownloadResult {
localPath: string;
fileSizeMb: number;
checksumValid: boolean | null;
checksumProvided: boolean;
message: string;
}
// -- Download implementation --------------------------------------------------
async function downloadFile(
url: string,
destPath: string,
checksumSha256?: string,
): Promise<DownloadResult> {
return new Promise((resolve, reject) => {
const hash = checksumSha256 ? crypto.createHash("sha256") : null;
const output = fs.createWriteStream(destPath);
let bytesDownloaded = 0;
const request = https.get(url, (res) => {
// Follow redirects (up to 5)
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
output.close();
fs.unlink(destPath, () => {});
// Recursively follow redirect
downloadFile(res.headers.location, destPath, checksumSha256)
.then(resolve)
.catch(reject);
return;
}
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
output.close();
fs.unlink(destPath, () => {});
reject(new Error(`HTTP ${res.statusCode ?? "unknown"} from ${url}`));
return;
}
res.on("data", (chunk: Buffer) => {
bytesDownloaded += chunk.length;
if (hash) hash.update(chunk);
});
res.pipe(output);
output.on("finish", () => {
output.close();
const fileSizeMb = Math.round((bytesDownloaded / (1024 * 1024)) * 100) / 100;
let checksumValid: boolean | null = null;
let message = `Downloaded ${fileSizeMb} MB to ${destPath}`;
if (checksumSha256 && hash) {
const actualHash = hash.digest("hex").toLowerCase();
const expected = checksumSha256.toLowerCase();
checksumValid = actualHash === expected;
if (!checksumValid) {
fs.unlink(destPath, () => {});
message = `SHA-256 mismatch. Expected: ${expected}. Got: ${actualHash}. File removed.`;
} else {
message = `Downloaded ${fileSizeMb} MB. SHA-256 verified OK.`;
}
}
resolve({
localPath: checksumValid === false ? "" : destPath,
fileSizeMb,
checksumValid,
checksumProvided: !!checksumSha256,
message,
});
});
output.on("error", (err) => {
fs.unlink(destPath, () => {});
reject(err);
});
});
request.on("error", (err) => {
fs.unlink(destPath, () => {});
reject(err);
});
request.setTimeout(300_000, () => {
request.destroy();
fs.unlink(destPath, () => {});
reject(new Error("Download timed out after 5 minutes"));
});
});
}
// -- Exported run function ----------------------------------------------------
export async function run({
url,
filename,
checksumSha256,
}: {
url: string;
filename?: string;
checksumSha256?: string;
}): Promise<DownloadResult> {
// Security: only HTTPS
let parsedUrl: URL;
try {
parsedUrl = new URL(url);
} catch {
throw new Error(`[download_installer] Invalid URL: ${url}`);
}
if (parsedUrl.protocol !== "https:") {
throw new Error(
`[download_installer] Only HTTPS URLs are allowed. Got: ${parsedUrl.protocol}`,
);
}
const resolvedFilename = filename ?? (nodePath.basename(parsedUrl.pathname) || "installer");
const destPath = nodePath.join(os.tmpdir(), resolvedFilename);
return downloadFile(url, destPath, checksumSha256);
}
// -- CLI smoke test -----------------------------------------------------------
if (false) {
run({ url: "https://example.com/installer.dmg" })
.then(r => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}