/**
* 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 ); });
}