/**
* mcp/skills/renewDhcpLease.ts — renew_dhcp_lease skill
*
* Releases the current DHCP IP address and requests a new lease.
* Use when the device has an incorrect IP, APIPA address (169.254.x.x),
* or network access was just restored.
*
* Platform strategy
* -----------------
* darwin `sudo ipconfig set {iface} DHCP` or ifconfig down/up cycle
* win32 PowerShell ipconfig /release && ipconfig /renew
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/renewDhcpLease.ts
*/
import * as os from "os" ;
import { exec } from "child_process" ;
import { promisify } from "util" ;
import { z } from "zod" ;
const execAsync = promisify (exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "renew_dhcp_lease" ,
description:
"Releases the current DHCP IP address and requests a new lease. " +
"Use when the device has an incorrect IP, APIPA address (169.254.x.x), " +
"or network access was just restored." ,
riskLevel: "medium" ,
destructive: false ,
requiresConsent: false ,
supportsDryRun: false ,
affectedScope: [ "network" , "system" ],
auditRequired: true ,
escalationHint: {
darwin: "sudo ipconfig set en0 DHCP # substitute the active interface name" ,
win32: "ipconfig /release && ipconfig /renew # run from elevated Command Prompt" ,
},
schema: {
interface: z
. string ()
. optional ()
. describe ( "Network interface name (e.g. 'en0', 'Wi-Fi'). Omit to renew all active interfaces" ),
},
} as const ;
// -- Types --------------------------------------------------------------------
interface RenewResult {
interface : string ;
previousIp : string | null ;
newIp : string | null ;
renewed : boolean ;
message : string ;
}
// -- PowerShell helper --------------------------------------------------------
async function runPS ( script : string ) : Promise < string > {
const encoded = Buffer. from (script, "utf16le" ). toString ( "base64" );
const { stdout } = await execAsync (
`powershell.exe -NoProfile -NonInteractive -EncodedCommand ${ encoded }` ,
{ maxBuffer: 10 * 1024 * 1024 },
);
return stdout. trim ();
}
// -- Helpers ------------------------------------------------------------------
async function getCurrentIpDarwin ( iface : string ) : Promise < string | null > {
try {
const { stdout } = await execAsync (
`ifconfig '${ iface . replace ( / ' / g , "' \\ ''" ) }' 2>/dev/null` ,
{ timeout: 5_000 },
);
const m = stdout. match ( / inet ( \d + \. \d + \. \d + \. \d + ) / );
return m?.[ 1 ] ?? null ;
} catch {
return null ;
}
}
async function getActiveInterfacesDarwin () : Promise < string []> {
try {
const { stdout } = await execAsync ( "ifconfig -l 2>/dev/null" , { timeout: 5_000 });
const all = stdout. trim (). split ( / \s + / );
// Only include Ethernet / Wi-Fi interfaces (en0, en1, …). These are the
// only interface types that hold DHCP leases on macOS. VPN tunnels
// (utun*), virtual bridges (bridge*), and wireless-peer links (awdl*,
// llw*) do not use DHCP and would stall for the full 15 s timeout each.
const dhcp = all. filter (( n ) => / ^ en \d +$ / . test (n));
return dhcp. length > 0 ? dhcp : [ "en0" ];
} catch {
return [ "en0" ];
}
}
// -- darwin implementation ----------------------------------------------------
async function renewDarwin ( iface : string | undefined ) : Promise < RenewResult []> {
const interfaces = iface ? [iface] : await getActiveInterfacesDarwin ();
const results : RenewResult [] = [];
for ( const ifName of interfaces) {
const safeName = ifName. replace ( / ' / g , "' \\ ''" );
const previousIp = await getCurrentIpDarwin (ifName);
try {
await execAsync (
`sudo ipconfig set '${ safeName }' DHCP 2>/dev/null` ,
{ timeout: 15000 },
);
// Allow a moment for lease to be acquired
await new Promise ( r => setTimeout (r, 2000 ));
const newIp = await getCurrentIpDarwin (ifName);
results. push ({
interface: ifName,
previousIp,
newIp,
renewed: true ,
message: newIp
? `DHCP lease renewed on ${ ifName }. New IP: ${ newIp }`
: `DHCP renewal sent on ${ ifName } but no IP assigned yet` ,
});
} catch (err) {
const msg = (err as Error ).message ?? String (err);
results. push ({
interface: ifName,
previousIp,
newIp: null ,
renewed: false ,
message: `Failed to renew DHCP on ${ ifName }: ${ msg }` ,
});
}
}
return results;
}
// -- win32 implementation -----------------------------------------------------
async function renewWin32 ( iface : string | undefined ) : Promise < RenewResult []> {
const adapterFilter = iface ? `"${ iface }"` : "*" ;
let previousIp : string | null = null ;
try {
const ps = `(Get-NetIPAddress -InterfaceAlias ${ adapterFilter } -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -First 1).IPAddress` ;
previousIp = ( await runPS (ps)) || null ;
} catch { /* ignore */ }
try {
const releaseCmd = iface
? `ipconfig /release "${ iface }"`
: "ipconfig /release" ;
const renewCmd = iface
? `ipconfig /renew "${ iface }"`
: "ipconfig /renew" ;
await execAsync ( `${ releaseCmd } && ${ renewCmd }` , { timeout: 30000 });
let newIp : string | null = null ;
try {
const ps2 = `(Get-NetIPAddress -InterfaceAlias ${ adapterFilter } -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -First 1).IPAddress` ;
newIp = ( await runPS (ps2)) || null ;
} catch { /* ignore */ }
return [{
interface: iface ?? "all" ,
previousIp,
newIp,
renewed: true ,
message: newIp
? `DHCP lease renewed. New IP: ${ newIp }`
: "DHCP renewal completed but no IP assigned yet" ,
}];
} catch (err) {
return [{
interface: iface ?? "all" ,
previousIp,
newIp: null ,
renewed: false ,
message: `Failed to renew DHCP: ${ ( err as Error ). message }` ,
}];
}
}
// -- Exported run function ----------------------------------------------------
export async function run ({
interface : iface ,
} : {
interface ?: string ;
} = {}) : Promise <{ platform : string ; results : RenewResult [] }> {
const platform = os. platform ();
const results = platform === "win32"
? await renewWin32 (iface)
: await renewDarwin (iface);
return { platform, results };
}
// -- CLI smoke test -----------------------------------------------------------
if ( false ) {
run ({})
. then ( r => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}