get_wifi_info

get_wifi_info

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.

Metadata

Name

get_wifi_info

Updated

3 weeks ago

Source

GitHub source

Risk

Low

Requires consent

false

Affected scope

user

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  Probe sequence (no single CLI 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. CoreWLAN via JXA (`osascript -l JavaScript`) — PRIMARY signal
 *              source. RSSI / noise / channel / band / txRate / security are
 *              NOT gated behind Location Services, so this returns real signal
 *              data even when the calling app lacks the CoreLocation grant.
 *           4. `system_profiler SPAirPortDataType` — FALLBACK only (used when
 *              the CoreWLAN probe fails to produce an RSSI).
 *         The legacy `airport -I` was deprecated in macOS 14.4 (returns only a
 *         deprecation warning) — DO NOT USE. `system_profiler`'s "Current
 *         Network Information" block (SSID *and* signal) is itself gated behind
 *         CoreLocation on macOS 14+, so relying on it alone returned all-null
 *         signal on machines where the agent lacks the location grant — which
 *         is exactly why CoreWLAN is now primary. SSID / BSSID remain
 *         location-gated on EVERY API; when withheld, `ssid` is null and
 *         `ssidAvailable` is false while RSSI / linkQuality are still accurate.
 * 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, execFile }    from "child_process";
import { promisify }         from "util";
import { z }                 from "zod";

const execAsync     = promisify(exec);
const execFileAsync = promisify(execFile);

// -- 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 {
  device:        string | null;
  ssid:          string | null;
  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 ----------------------------------------------------

async function findWifiDeviceDarwin(): Promise<string | null> {
  try {
    const { stdout } = await execAsync("networksetup -listallhardwareports");
    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;
  }
}

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 };
  }
}

async function getWifiInfoFromCoreWLAN(): Promise<CoreWlanWifiInfo | null> {
  const jxa = `
    ObjC.import('CoreWLAN');
    var i = $.CWWiFiClient.sharedWiFiClient.interface;
    function n(v){ v = (v && v.js !== undefined) ? v.js : v; var x = Number(v); return isNaN(x) ? null : x; }
    function s(v){ return v ? ObjC.unwrap(v) : null; }
    var ch = i.wlanChannel;
    JSON.stringify({
      ssid: s(i.ssid), bssid: s(i.bssid),
      rssi: n(i.rssiValue), noise: n(i.noiseMeasurement), txRate: n(i.transmitRate),
      channel: ch ? n(ch.channelNumber) : null,
      band: ch ? n(ch.channelBand) : null,
      security: n(i.security)
    });
  `.replace(/\n\s*/g, " ").trim();

try {
    const { stdout } = await execFileAsync(
      "osascript", ["-l", "JavaScript", "-e", jxa], { timeout: 5_000 },
    );
    const r = JSON.parse(stdout.trim()) as {
      ssid: string | null; bssid: string | null;
      rssi: number | null; noise: number | null; txRate: number | null;
      channel: number | null; band: number | null; security: number | null;
    };
    return {
      ssid:       r.ssid || null,
      bssid:      r.bssid || null,
      rssi:       r.rssi  && r.rssi  !== 0 ? r.rssi  : null,
      noise:      r.noise && r.noise !== 0 ? r.noise : null,
      txRateMbps: r.txRate && r.txRate > 0 ? r.txRate : null,
      channel:    r.channel && r.channel > 0 ? r.channel : null,
      band:       r.band != null ? (CW_BAND[r.band] ?? null) : null,
      security:   r.security != null ? (CW_SECURITY[r.security] ?? null) : null,
    };
  } catch {
    return null;
  }
}

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");
  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;
  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);
  }

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";
    }
  }

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

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);

const link = await probeWifiLinkDarwin(device);
  if (!link.up || !link.hasIp) return empty(device);

const cw = await getWifiInfoFromCoreWLAN();
  const sp = (!cw || cw.rssi === null) ? await getWifiInfoFromSystemProfiler() : null;

const rssi   = cw?.rssi  ?? sp?.rssi  ?? null;
  const noise  = cw?.noise ?? null;
  const ssid   = cw?.ssid  ?? sp?.ssid ?? null;
  const channel = cw?.channel ?? sp?.channel ?? null;

return {
    device,
    ssid,
    ssidAvailable: Boolean(ssid),
    bssid:         cw?.bssid ?? null,
    rssi,
    noise,
    snr:           rssi !== null && noise !== null ? rssi - noise : null,
    channel,
    band:          cw?.band ?? sp?.band
                     ?? (channel !== null ? (channel <= 14 ? "2.4 GHz" : "5 GHz") : null),
    security:      cw?.security ?? sp?.security ?? null,
    txRateMbps:    cw?.txRateMbps ?? sp?.txRateMbps ?? null,
    linkQuality:   computeLinkQuality(rssi),
    isConnected:   true,
    platform:      "darwin",
  };
}

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)");

let rssi: number | null = null;
  if (sigStr) {
    const sigPct = parseInt(sigStr.replace("%", ""), 10);
    if (!isNaN(sigPct)) {
      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,
    ssidAvailable: ssid !== null,
    bssid,
    rssi,
    noise:      null,
    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); });
}