Code

/**
 * mcp/skills/getWifiInfo.ts — get_wifi_info skill
 *
 * Reports current Wi-Fi connection details: SSID, signal strength (RSSI dBm),
 * channel, band, security type, and link speed.  Use to diagnose Wi-Fi
 * performance or intermittent connectivity.
 *
 * Platform strategy
 * -----------------
 * darwin  Three-step probe (no single command works post-Sequoia):
 *           1. `networksetup -listallhardwareports` — discover the Wi-Fi
 *              device (typically en0 but not guaranteed)
 *           2. `ifconfig <device>` — confirm the interface is UP+RUNNING with
 *              an inet address (the authoritative isConnected signal)
 *           3. `system_profiler SPAirPortDataType` — rich per-network details
 *              (SSID, channel, band, RSSI, security)
 *         The legacy `airport -I` was deprecated in macOS 14.4 and now returns
 *         only a deprecation warning with no key:value data — DO NOT USE.
 *         The SSID is reported as `<redacted>` unless the calling app holds
 *         CoreLocation authorization (NSLocationWhenInUseUsageDescription +
 *         user grant).  When redacted, `ssid` is null and `ssidAvailable` is
 *         false — connectivity reporting is still accurate via ifconfig.
 * win32   PowerShell `netsh wlan show interfaces` — parses text output
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/getWifiInfo.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: "get_wifi_info",
  description:
    "Reports current Wi-Fi connection details: SSID, signal strength (RSSI dBm), " +
    "channel, band, security type, and link speed. " +
    "Use to diagnose Wi-Fi performance or intermittent connectivity.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {},
} as const;
 
// -- Types --------------------------------------------------------------------
 
type LinkQuality = "excellent" | "good" | "fair" | "poor" | "unknown";
 
interface WifiInfoResult {
  /** OS-level Wi-Fi device name (e.g. "en0").  Resolved at runtime on darwin
   *  via networksetup; null when no Wi-Fi hardware port is found. */
  device:        string | null;
  ssid:          string | null;
  /** False when the OS withholds the SSID — typically because the calling
   *  app lacks CoreLocation authorization on macOS 14.4+.  `isConnected` is
   *  still authoritative and correct in that case; the SSID is just hidden. */
  ssidAvailable: boolean;
  bssid:         string | null;
  rssi:          number | null;
  noise:         number | null;
  snr:           number | null;
  channel:       number | null;
  band:          string | null;
  security:      string | null;
  txRateMbps:    number | null;
  linkQuality:   LinkQuality;
  isConnected:   boolean;
  platform:      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 ------------------------------------------------------------------
 
function computeLinkQuality(rssi: number | null): LinkQuality {
  if (rssi === null) return "unknown";
  if (rssi > -50)   return "excellent";
  if (rssi > -60)   return "good";
  if (rssi > -70)   return "fair";
  return "poor";
}
 
function parseKeyValue(output: string): Map<string, string> {
  const map = new Map<string, string>();
  for (const line of output.split("\n")) {
    const colonIdx = line.indexOf(":");
    if (colonIdx === -1) continue;
    const key   = line.slice(0, colonIdx).trim();
    const value = line.slice(colonIdx + 1).trim();
    if (key) map.set(key, value);
  }
  return map;
}
 
// -- darwin implementation ----------------------------------------------------
 
/**
 * Discovers the Wi-Fi hardware port's device name (e.g. "en0").
 * On Macs with discrete Wi-Fi cards or unusual configurations the device
 * name may not be en0, so we never hard-code it.  Returns null when no
 * Wi-Fi hardware port exists on this machine.
 */
async function findWifiDeviceDarwin(): Promise<string | null> {
  try {
    const { stdout } = await execAsync("networksetup -listallhardwareports");
    // Output is blocks of three lines separated by blank lines:
    //   Hardware Port: Wi-Fi
    //   Device: en0
    //   Ethernet Address: 84:2f:57:1e:fc:28
    const blocks = stdout.split(/\n\s*\n/);
    for (const block of blocks) {
      const portMatch   = block.match(/Hardware Port:\s*(.+)/);
      const deviceMatch = block.match(/Device:\s*(\S+)/);
      if (portMatch && deviceMatch && portMatch[1].trim() === "Wi-Fi") {
        return deviceMatch[1].trim();
      }
    }
    return null;
  } catch {
    return null;
  }
}
 
/**
 * Authoritative connectivity probe: parses `ifconfig <device>` and reports
 * whether the interface is UP+RUNNING with an inet address.  This works
 * regardless of CoreLocation authorization, so even when the SSID is
 * redacted we can still correctly report `isConnected`.
 */
async function probeWifiLinkDarwin(device: string): Promise<{
  up:    boolean;
  hasIp: boolean;
  ipv4:  string | null;
}> {
  try {
    const { stdout } = await execAsync(`ifconfig '${device}'`);
    const flags = stdout.match(/flags=\S+\s*<([^>]*)>/)?.[1] ?? "";
    const flagSet = new Set(flags.split(","));
    const up = flagSet.has("UP") && flagSet.has("RUNNING");
    const ipv4Match = stdout.match(/inet (\d+\.\d+\.\d+\.\d+)/);
    return { up, hasIp: !!ipv4Match, ipv4: ipv4Match?.[1] ?? null };
  } catch {
    return { up: false, hasIp: false, ipv4: null };
  }
}
 
interface SystemProfilerWifiInfo {
  ssid:           string | null;
  ssidAvailable:  boolean;
  channel:        number | null;
  band:           string | null;
  rssi:           number | null;
  txRateMbps:     number | null;
  security:       string | null;
}
 
/**
 * Parses the "Current Network Information" block out of
 * `system_profiler SPAirPortDataType`.  Returns null when the section is
 * absent (no Wi-Fi association at the moment of probe).
 *
 * The SSID line appears as either the network name (when CoreLocation is
 * authorized) or `<redacted>` (when not).  We surface the redaction state
 * via `ssidAvailable: false` so callers can distinguish "really no Wi-Fi"
 * from "Wi-Fi is fine, OS is just hiding the name".
 */
async function getWifiInfoFromSystemProfiler(): Promise<SystemProfilerWifiInfo | null> {
  let output = "";
  try {
    const { stdout } = await execAsync(
      "system_profiler SPAirPortDataType",
      { maxBuffer: 5 * 1024 * 1024, timeout: 10_000 },
    );
    output = stdout;
  } catch {
    return null;
  }
 
  const idx = output.indexOf("Current Network Information:");
  if (idx === -1) return null;
 
  const lines = output.slice(idx).split("\n");
 
  // The SSID is the first non-blank indented line after the header, ending
  // with a colon.  Skip the header itself (lines[0]).
  let ssidLine: string | null = null;
  let ssidLineIdx = -1;
  for (let i = 1; i < lines.length && i < 8; i++) {
    const trimmed = lines[i].trim();
    if (trimmed.length === 0) continue;
    if (trimmed.endsWith(":")) {
      ssidLine = trimmed.replace(/:\s*$/, "");
      ssidLineIdx = i;
      break;
    }
  }
  if (!ssidLine || ssidLineIdx === -1) return null;
 
  const isRedacted     = ssidLine === "<redacted>";
  const ssid           = isRedacted ? null : ssidLine;
  const ssidAvailable  = !isRedacted;
 
  // Subsequent lines are key:value pairs nested under the SSID.  Collect
  // until we hit a blank line, an unindented line, or another SSID-style
  // header (rare but possible when multiple network interfaces are reported).
  const kv = new Map<string, string>();
  for (let i = ssidLineIdx + 1; i < lines.length; i++) {
    const raw = lines[i];
    if (raw.trim().length === 0) break;
    if (!/^\s/.test(raw)) break;
    const colonIdx = raw.indexOf(":");
    if (colonIdx === -1) continue;
    const key = raw.slice(0, colonIdx).trim();
    const value = raw.slice(colonIdx + 1).trim();
    if (key && value) kv.set(key, value);
  }
 
  // Channel example: "136 (5GHz, 80MHz)" or "6 (2GHz, 20MHz)" or just "6"
  let channel: number | null = null;
  let band:    string | null = null;
  const channelRaw = kv.get("Channel");
  if (channelRaw) {
    const m = channelRaw.match(/^(\d+)(?:\s*\(([^)]+)\))?/);
    if (m) {
      channel = parseInt(m[1], 10);
      const bandHint = m[2] ?? "";
      if (bandHint.includes("6GHz"))      band = "6 GHz";
      else if (bandHint.includes("5GHz")) band = "5 GHz";
      else if (bandHint.includes("2GHz") || bandHint.includes("2.4GHz")) band = "2.4 GHz";
      else                                band = channel <= 14 ? "2.4 GHz" : "5 GHz";
    }
  }
 
  // Signal / Noise example: "-58 dBm / -89 dBm"
  let rssi: number | null = null;
  const signalRaw = kv.get("Signal / Noise");
  if (signalRaw) {
    const m = signalRaw.match(/(-?\d+)\s*dBm/);
    if (m) rssi = parseInt(m[1], 10);
  }
 
  // Transmit Rate example: "650" or "0.0"
  let txRateMbps: number | null = null;
  const txRaw = kv.get("Transmit Rate") ?? kv.get("Last Tx Rate");
  if (txRaw) {
    const num = parseFloat(txRaw);
    if (!isNaN(num) && num > 0) txRateMbps = num;
  }
 
  return {
    ssid,
    ssidAvailable,
    channel,
    band,
    rssi,
    txRateMbps,
    security: kv.get("Security") ?? null,
  };
}
 
async function getWifiInfoDarwin(): Promise<WifiInfoResult> {
  const empty = (device: string | null): WifiInfoResult => ({
    device,
    ssid:          null,
    ssidAvailable: false,
    bssid:         null,
    rssi:          null,
    noise:         null,
    snr:           null,
    channel:       null,
    band:          null,
    security:      null,
    txRateMbps:    null,
    linkQuality:   "unknown",
    isConnected:   false,
    platform:      "darwin",
  });
 
  const device = await findWifiDeviceDarwin();
  if (!device) return empty(null);
 
  // Step 1 — authoritative connectivity check.  When the interface is down
  // or has no IP, we know definitively that Wi-Fi is not active and can
  // skip the slower system_profiler call (~1-3s on busy systems).
  const link = await probeWifiLinkDarwin(device);
  if (!link.up || !link.hasIp) return empty(device);
 
  // Step 2 — interface is up; pull rich details.  system_profiler may fail
  // (privacy permission, transient I/O) — when that happens we still report
  // isConnected: true, just without channel/band/RSSI enrichment.
  const info = await getWifiInfoFromSystemProfiler();
 
  return {
    device,
    ssid:          info?.ssid ?? null,
    // SSID-available is true only when system_profiler returned a real name.
    // When system_profiler itself failed, we don't know — surface as false
    // (caller should treat as "unable to obtain" rather than "redacted").
    ssidAvailable: info?.ssidAvailable ?? false,
    bssid:         null, // BSSID is also gated behind CoreLocation; not parsed.
    rssi:          info?.rssi ?? null,
    noise:         null, // Not exposed by system_profiler.
    snr:           null,
    channel:       info?.channel ?? null,
    band:          info?.band ?? null,
    security:      info?.security ?? null,
    txRateMbps:    info?.txRateMbps ?? null,
    linkQuality:   computeLinkQuality(info?.rssi ?? null),
    isConnected:   true, // Link is UP+RUNNING with an IP — that's connected.
    platform:      "darwin",
  };
}
 
// -- win32 implementation -----------------------------------------------------
 
async function getWifiInfoWin32(): Promise<WifiInfoResult> {
  let output = "";
  try {
    const ps = `
$ErrorActionPreference = 'SilentlyContinue'
netsh wlan show interfaces`.trim();
    output = await runPS(ps);
  } catch {
    return {
      device: null, ssid: null, ssidAvailable: false, bssid: null,
      rssi: null, noise: null, snr: null,
      channel: null, band: null, security: null, txRateMbps: null,
      linkQuality: "unknown", isConnected: false, platform: "win32",
    };
  }
 
  const kv = parseKeyValue(output);
 
  const device   = kv.get("Name") ?? null;
  const ssid     = kv.get("SSID") ?? kv.get("      SSID") ?? null;
  const bssid    = kv.get("BSSID") ?? null;
  const sigStr   = kv.get("Signal");
  const chanStr  = kv.get("Channel");
  const radioType = kv.get("Radio type") ?? null;
  const auth     = kv.get("Authentication") ?? null;
  const rxStr    = kv.get("Receive rate (Mbps)");
  const txStr    = kv.get("Transmit rate (Mbps)");
 
  // Signal on Windows is 0-100 — convert to approximate RSSI
  let rssi: number | null = null;
  if (sigStr) {
    const sigPct = parseInt(sigStr.replace("%", ""), 10);
    if (!isNaN(sigPct)) {
      // Approximate RSSI from percentage: 100% ~ -50dBm, 0% ~ -100dBm
      rssi = Math.round((sigPct / 2) - 100);
    }
  }
 
  const channel    = chanStr ? parseInt(chanStr, 10) : null;
  const band       = radioType?.includes("802.11a") || radioType?.includes("802.11n") || radioType?.includes("802.11ac")
    ? (channel && channel > 14 ? "5 GHz" : "2.4 GHz")
    : null;
 
  const txRateMbps = txStr ? parseFloat(txStr) : (rxStr ? parseFloat(rxStr) : null);
  const isConnected = ssid !== null && ssid !== "";
 
  return {
    device,
    ssid,
    // Windows netsh does not redact the SSID — its presence is the
    // authoritative signal.  When ssid is null we couldn't read it at all.
    ssidAvailable: ssid !== null,
    bssid,
    rssi,
    noise:      null, // Not available on Windows via netsh
    snr:        null,
    channel:    isNaN(channel ?? NaN) ? null : channel,
    band,
    security:   auth,
    txRateMbps: isNaN(txRateMbps ?? NaN) ? null : txRateMbps,
    linkQuality: computeLinkQuality(rssi),
    isConnected,
    platform: "win32",
  };
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run(_args: Record<string, never> = {}) {
  const platform = os.platform();
  return platform === "win32"
    ? getWifiInfoWin32()
    : getWifiInfoDarwin();
}
 
// -- Smoke test ---------------------------------------------------------------
 
if (false) {
  run({})
    .then(r => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}