list_bluetooth_devices
list_bluetooth_devices
Enumerates paired Bluetooth devices (connected and offline) with name, address, connection state, and where supported RSSI + battery percent. Use when troubleshooting flaky Bluetooth peripherals — a paired device showing as not-connected when the user expects it to be active is the primary signal. Read-only.
Metadata
Name
list_bluetooth_devices
Updated
2 months ago
Source
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/listBluetoothDevices.ts — list_bluetooth_devices skill
*
* Enumerates paired Bluetooth devices with name, address, connection
* state, and (where surfaced by the OS) RSSI + battery level. Used by
* the P0-d A/V & Peripheral Repair skill to flag flaky / disconnected
* Bluetooth peripherals (AirPods, mice, keyboards) before deciding
* whether to reset the Bluetooth module.
*
* Platform strategy
* -----------------
* darwin `system_profiler SPBluetoothDataType -json`. The macOS report
* has a specific shape: a tree where the top-level entry has
* "device_connected" (paired AND currently connected) and
* "device_not_connected" (paired but offline) sub-objects.
* win32 PowerShell `Get-PnpDevice -Class Bluetooth` for the device
* list; battery + RSSI are not consistently exposed by Windows
* APIs from PowerShell, so those fields are reported as
* undefined on win32 unless `BluetoothLEAdvertisement` reports
* them (out of scope for the alpha).
*
* Read-only — device pair / unpair / module reset are separate tools.
*/
import * as os from "os";
import { z } from "zod";
import {
execAsync,
runPS,
} from "./_shared/platform";
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "list_bluetooth_devices",
description:
"Enumerates paired Bluetooth devices (connected and offline) with name, " +
"address, connection state, and where supported RSSI + battery percent. " +
"Use when troubleshooting flaky Bluetooth peripherals — a paired device " +
"showing as not-connected when the user expects it to be active is the " +
"primary signal. Read-only.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
schema: {} as Record<string, z.ZodTypeAny>,
} as const;
// -- Types --------------------------------------------------------------------
export interface BluetoothDevice {
name: string;
address?: string;
/** Whether the device is currently connected. */
connected: boolean;
/** Whether the device is paired (always true on darwin output, may be inferred on win32). */
paired: boolean;
/** RSSI in dBm if reported by the OS. */
rssi?: number;
/** Battery percentage if reported. */
batteryPercent?: number;
/** Vendor ID where surfaced. */
vendorId?: string;
productId?: string;
}
export interface ListBluetoothDevicesResult {
platform: NodeJS.Platform;
poweredOn: boolean;
total: number;
devices: BluetoothDevice[];
}
// -- darwin implementation ----------------------------------------------------
interface SPBluetoothEntry {
/** Each device is a single-key object: { "<deviceName>": { ...details } } */
[name: string]: {
device_address?: string;
device_minorClassOfDevice_string?: string;
device_majorClassOfDevice_string?: string;
device_battery_level_main?: string; // "85%"
device_rssi?: string;
device_vendorID?: string;
device_productID?: string;
};
}
interface SPBluetoothControllerInfo {
controller_state?: string; // "On" | "Off"
device_title?: string;
general_device_status?: string;
}
interface SPBluetoothBlock {
device_title?: string;
controller_properties?: SPBluetoothControllerInfo;
device_connected?: SPBluetoothEntry[];
device_not_connected?: SPBluetoothEntry[];
}
interface SPBluetoothData {
SPBluetoothDataType?: SPBluetoothBlock[];
}
function parseBatteryDarwin(raw: string | undefined): number | undefined {
if (!raw) return undefined;
const match = raw.match(/(\d+)\s*%?/);
if (!match) return undefined;
const n = parseInt(match[1], 10);
return Number.isNaN(n) ? undefined : n;
}
function parseRssiDarwin(raw: string | undefined): number | undefined {
if (!raw) return undefined;
const match = raw.match(/-?\d+/);
if (!match) return undefined;
const n = parseInt(match[0], 10);
return Number.isNaN(n) ? undefined : n;
}
function flattenDarwinDevices(entries: SPBluetoothEntry[], connected: boolean, out: BluetoothDevice[]): void {
for (const entry of entries) {
for (const [name, details] of Object.entries(entry)) {
const battery = parseBatteryDarwin(details.device_battery_level_main);
const rssi = parseRssiDarwin(details.device_rssi);
const dev: BluetoothDevice = {
name,
...(details.device_address && { address: details.device_address }),
connected,
paired: true,
...(battery !== undefined && { batteryPercent: battery }),
...(rssi !== undefined && { rssi }),
...(details.device_vendorID && { vendorId: details.device_vendorID }),
...(details.device_productID && { productId: details.device_productID }),
};
out.push(dev);
}
}
}
function parseDarwinOutput(stdout: string): ListBluetoothDevicesResult {
const data = JSON.parse(stdout) as SPBluetoothData;
const blocks = data.SPBluetoothDataType ?? [];
const devices: BluetoothDevice[] = [];
let poweredOn = false;
for (const block of blocks) {
if (block.controller_properties?.controller_state === "On") poweredOn = true;
if (block.device_connected) flattenDarwinDevices(block.device_connected, true, devices);
if (block.device_not_connected) flattenDarwinDevices(block.device_not_connected, false, devices);
}
return {
platform: "darwin",
poweredOn,
total: devices.length,
devices,
};
}
async function listBluetoothDarwin(): Promise<ListBluetoothDevicesResult> {
const { stdout } = await execAsync(
"system_profiler SPBluetoothDataType -json 2>/dev/null",
{ maxBuffer: 10 * 1024 * 1024 },
);
return parseDarwinOutput(stdout);
}
// -- win32 implementation -----------------------------------------------------
interface WinBluetoothDevice {
Name: string;
Class: string;
Status: string; // "OK" | "Error" | "Unknown"
Present: boolean;
InstanceId?: string; // contains BTHENUM\Dev_<address>
Manufacturer?: string;
}
function extractWinAddress(instanceId: string | undefined): string | undefined {
if (!instanceId) return undefined;
// BTHENUM\Dev_<12-hex-digit-address>\...
const match = instanceId.match(/Dev_([0-9A-Fa-f]{12})/);
if (!match) return undefined;
// Format as XX:XX:XX:XX:XX:XX
const hex = match[1].toUpperCase();
return `${hex.slice(0,2)}:${hex.slice(2,4)}:${hex.slice(4,6)}:${hex.slice(6,8)}:${hex.slice(8,10)}:${hex.slice(10,12)}`;
}
function parseWinOutput(stdout: string, controllerOk: boolean): ListBluetoothDevicesResult {
const parsed = stdout.trim()
? (JSON.parse(stdout) as WinBluetoothDevice | WinBluetoothDevice[])
: [];
const raw: WinBluetoothDevice[] = Array.isArray(parsed) ? parsed : [parsed];
const devices: BluetoothDevice[] = raw
.filter((d) => d?.Name && d.Class === "Bluetooth")
.map((d) => {
const address = extractWinAddress(d.InstanceId);
// PnP "Present + Status=OK" maps to connected; "Present + Status=Unknown"
// typically means paired-but-asleep (e.g. AirPods in case).
const connected = d.Present && d.Status === "OK";
const paired = d.Present;
return {
name: d.Name,
...(address && { address }),
connected,
paired,
};
});
return {
platform: "win32",
poweredOn: controllerOk,
total: devices.length,
devices,
};
}
async function listBluetoothWin32(): Promise<ListBluetoothDevicesResult> {
const devicesScript = `
$ErrorActionPreference = 'SilentlyContinue'
Get-PnpDevice -Class Bluetooth |
Select-Object Name, Class, Status, Present, InstanceId, Manufacturer |
ConvertTo-Json -Depth 2 -Compress`.trim();
const controllerScript = `
$ErrorActionPreference = 'SilentlyContinue'
$svc = Get-Service -Name bthserv -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq 'Running') { 'on' } else { 'off' }`.trim();
const [rawDevices, rawController] = await Promise.all([\
runPS(devicesScript),\
runPS(controllerScript),\
]);
const controllerOk = rawController.trim().toLowerCase() === "on";
return parseWinOutput(rawDevices, controllerOk);
}
// -- Exported run function ----------------------------------------------------
export async function run(): Promise<ListBluetoothDevicesResult> {
const platform = os.platform();
if (platform === "darwin") return listBluetoothDarwin();
if (platform === "win32") return listBluetoothWin32();
throw new Error(`list_bluetooth_devices: unsupported platform "${platform}"`);
}
// -- Test helpers -------------------------------------------------------------
/** Exported for unit tests only — do not use from production code. */
export const __testing = {
parseDarwinOutput,
parseWinOutput,
parseBatteryDarwin,
parseRssiDarwin,
extractWinAddress,
flattenDarwinDevices,
};
``