/**
* mcp/skills/checkConnectivity.ts — check_connectivity skill
*
* Checks network connectivity by pinging multiple targets (gateway, DNS
* servers, and internet hosts). Returns reachability status for each target.
*
* Platform strategy
* -----------------
* darwin `ping -c {count} -W 2 {target}` — built-in BSD ping
* win32 PowerShell Test-Connection | ConvertTo-Json
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/checkConnectivity.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: "check_connectivity" ,
description:
"Checks network connectivity by pinging multiple targets (gateway, DNS " +
"servers, and internet hosts). Returns reachability status for each target. " +
"Use when user reports network issues, before/after VPN operations, or when " +
"diagnosing email/service connectivity problems." ,
riskLevel: "low" ,
destructive: false ,
requiresConsent: false ,
supportsDryRun: false ,
affectedScope: [ "user" ],
auditRequired: false ,
schema: {
targets: z
. array (z. string ())
. optional ()
. describe ( "Hosts to ping. Defaults to ['8.8.8.8', '1.1.1.1', 'google.com']" ),
count: z
. number ()
. optional ()
. describe ( "Ping count per target. Default: 3" ),
},
} as const ;
// -- Types --------------------------------------------------------------------
interface TargetResult {
host : string ;
reachable : boolean ;
packetLoss : number ;
avgRttMs : number | null ;
}
// -- 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: 20 * 1024 * 1024 , timeout: 20_000 },
);
return stdout. trim ();
}
// -- darwin implementation ----------------------------------------------------
async function pingTargetDarwin ( host : string , count : number ) : Promise < TargetResult > {
// Validate host to prevent shell injection — allow hostnames, IPv4, IPv6
if ( ! / ^ [a-zA-Z0-9.\-:] +$ / . test (host)) {
return { host, reachable: false , packetLoss: 100 , avgRttMs: null };
}
try {
const { stdout } = await execAsync (
`ping -c ${ count } -W 2 ${ host } 2>&1` ,
{ maxBuffer: 1 * 1024 * 1024 , timeout: 15_000 },
);
// Parse packet loss: "3 packets transmitted, 3 packets received, 0.0% packet loss"
const lossMatch = stdout. match ( / ( \d + (?: \. \d + ) ? )% \s + packet loss / );
const packetLoss = lossMatch ? parseFloat (lossMatch[ 1 ]) : 100 ;
// Parse RTT: "round-trip min/avg/max/stddev = 1.234/5.678/9.012/0.123 ms"
const rttMatch = stdout. match ( / min \/ avg \/ max \/ (?:stddev | mdev) \s * = \s * [\d.] + \/ ( [\d.] + ) / );
const avgRttMs = rttMatch ? parseFloat (rttMatch[ 1 ]) : null ;
return {
host,
reachable: packetLoss < 100 ,
packetLoss,
avgRttMs,
};
} catch (err) {
// ping exits non-zero when host is unreachable; stdout may still have loss info
const stdout = (err as { stdout ?: string }).stdout ?? "" ;
const lossMatch = stdout. match ( / ( \d + (?: \. \d + ) ? )% \s + packet loss / );
const packetLoss = lossMatch ? parseFloat (lossMatch[ 1 ]) : 100 ;
return { host, reachable: false , packetLoss, avgRttMs: null };
}
}
async function checkConnectivityDarwin (
targets : string [],
count : number ,
) : Promise < TargetResult []> {
return Promise . all (targets. map (( t ) => pingTargetDarwin (t, count)));
}
// -- win32 implementation -----------------------------------------------------
async function checkConnectivityWin32 (
targets : string [],
count : number ,
) : Promise < TargetResult []> {
const safeTargets = targets. filter (( t ) => / ^ [a-zA-Z0-9.\-:] +$ / . test (t));
const targetList = safeTargets. map (( t ) => `'${ t }'` ). join ( "," );
const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$results = @()
foreach ($host in @(${ targetList })) {
$pings = Test-Connection -ComputerName $host -Count ${ count } -ErrorAction SilentlyContinue
if ($null -eq $pings -or $pings.Count -eq 0) {
$results += [PSCustomObject]@{
host = $host
reachable = $false
packetLoss = 100
avgRttMs = $null
}
} else {
$received = ($pings | Where-Object { $_.StatusCode -eq 0 }).Count
$loss = [Math]::Round((1 - ($received / ${ count })) * 100, 1)
$avgRtt = if ($received -gt 0) { [Math]::Round(($pings | Where-Object { $_.StatusCode -eq 0 } | Measure-Object ResponseTime -Average).Average, 2) } else { $null }
$results += [PSCustomObject]@{
host = $host
reachable = ($received -gt 0)
packetLoss = $loss
avgRttMs = $avgRtt
}
}
}
$results | ConvertTo-Json -Depth 2 -Compress` . trim ();
const raw = await runPS (ps);
const parsed = JSON . parse (raw) as TargetResult | TargetResult [];
const arr = Array. isArray (parsed) ? parsed : [parsed];
// Re-add any filtered-out invalid hosts as unreachable
return targets. map (( host ) => {
const found = arr. find (( r ) => r.host === host);
return found ?? { host, reachable: false , packetLoss: 100 , avgRttMs: null };
});
}
// -- Exported run function ----------------------------------------------------
export async function run ({
targets = [ "8.8.8.8" , "1.1.1.1" , "google.com" ],
count = 3 ,
} : {
targets ?: string [];
count ?: number ;
} = {}) {
const platform = os. platform ();
const results = platform === "win32"
? await checkConnectivityWin32 (targets, count)
: await checkConnectivityDarwin (targets, count);
return {
platform,
targets: results,
allReachable: results. every (( r ) => r.reachable),
anyReachable: results. some (( r ) => r.reachable),
};
}
// -- 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 ); });
}