Files
mcp-windows/src/tools/wifi.ts
T
Jonathan Miller a2922eca6b
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
feat: implement v2.0 connectivity & hardware tools (7 new tools)
- tools/bluetooth.ts: windows_bluetooth_get, _control (#45, #46)
- tools/wifi.ts: windows_wifi_networks, _connect (#47, #48)
- tools/usb.ts: windows_usb_devices with safe eject (#49)
- tools/printer.ts: windows_printer_list with queue/default/clear (#50)
- tools/hosts.ts: windows_hosts_file list/add/remove/enable/disable (#51)

Total: 51 tools registered

Authored-by: Moko Consulting
2026-05-25 22:42:56 -05:00

158 lines
6.2 KiB
TypeScript

/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tools: windows_wifi_networks (#47), windows_wifi_connect (#48)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerWifiTools(server: McpServer): void {
server.tool(
'windows_wifi_networks',
'Scan and list available Wi-Fi networks with signal strength, security, and saved profiles.',
{
rescan: z.boolean().default(false).describe('Force a rescan before listing'),
saved: z.boolean().default(false).describe('List saved profiles instead of available networks'),
},
async ({ rescan, saved }) => {
if (saved) {
const ps = `netsh wlan show profiles | Select-String 'All User Profile' | ForEach-Object { ($_ -split ':')[1].Trim() }`;
const result = await runPowerShell(ps, { timeout: 10000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const profiles = result.stdout ? result.stdout.split('\n').filter(Boolean) : [];
return {
content: [{ type: 'text', text: `Saved Wi-Fi profiles (${profiles.length}):\n${profiles.map(p => ` ${p}`).join('\n')}` }],
};
}
const scanCmd = rescan ? `netsh wlan scan; Start-Sleep -Seconds 3;` : '';
const ps = `
${scanCmd}
$output = netsh wlan show networks mode=bssid 2>&1
$networks = [System.Collections.Generic.List[PSObject]]::new()
$current = @{}
foreach ($line in ($output -split [char]10)) {
$line = $line.Trim()
if ($line -match '^SSID \\d+ : (.*)') {
if ($current.SSID) { $networks.Add([PSCustomObject]$current) }
$current = @{ SSID = $Matches[1]; Signal = ''; Auth = ''; Channel = ''; BSSID = '' }
}
elseif ($line -match '^Signal\\s+: (.*)') { $current.Signal = $Matches[1] }
elseif ($line -match '^Authentication\\s+: (.*)') { $current.Auth = $Matches[1] }
elseif ($line -match '^Channel\\s+: (.*)') { $current.Channel = $Matches[1] }
elseif ($line -match '^BSSID \\d+\\s+: (.*)') { $current.BSSID = $Matches[1] }
}
if ($current.SSID) { $networks.Add([PSCustomObject]$current) }
# Current connection
$connected = (netsh wlan show interfaces | Select-String 'SSID\\s+:' | Select-Object -First 1) -replace '.*:\\s*',''
$networks | Sort-Object { [int]($_.Signal -replace '%','') } -Descending | ConvertTo-Json -Depth 3 -Compress
Write-Output "---CONNECTED:$($connected.Trim())"`;
const result = await runPowerShell(ps, { timeout: 20000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const outputLines = result.stdout.split('\n');
const connectedLine = outputLines.find(l => l.startsWith('---CONNECTED:'));
const connected = connectedLine ? connectedLine.replace('---CONNECTED:', '').trim() : '';
const jsonLine = outputLines.filter(l => !l.startsWith('---CONNECTED:')).join('\n');
let networks: Array<{ SSID: string; Signal: string; Auth: string; Channel: string }> = [];
if (jsonLine.trim()) {
try {
const parsed = JSON.parse(jsonLine);
networks = Array.isArray(parsed) ? parsed : [parsed];
} catch { /* empty */ }
}
const lines = networks.map(n => {
const icon = n.SSID === connected ? ' *' : ' ';
return `${icon} ${n.Signal.padStart(4)} ${n.Auth.padEnd(18)} Ch ${(n.Channel || '?').padEnd(3)} ${n.SSID}`;
});
const header = ` ${'Sig'.padStart(4)} ${'Security'.padEnd(18)} ${'Ch'.padEnd(5)} SSID`;
return {
content: [{
type: 'text',
text: `Connected: ${connected || '(none)'}\n\n${header}\n${'─'.repeat(65)}\n${lines.join('\n')}\n\n${networks.length} networks (* = connected)`,
}],
};
},
);
server.tool(
'windows_wifi_connect',
'Connect to a Wi-Fi network, disconnect, or forget a saved profile.',
{
action: z.enum(['connect', 'disconnect', 'forget']).describe('Action'),
ssid: z.string().optional().describe('Network SSID (for connect/forget)'),
password: z.string().optional().describe('Password (for connecting to a new network)'),
},
async ({ action, ssid, password }) => {
let ps: string;
switch (action) {
case 'disconnect':
ps = `netsh wlan disconnect; "Disconnected from Wi-Fi"`;
break;
case 'forget':
if (!ssid) {
return { content: [{ type: 'text', text: 'Forget requires ssid.' }], isError: true };
}
ps = `netsh wlan delete profile name="${ssid.replace(/"/g, '""')}" 2>&1; "Forgot network: ${ssid}"`;
break;
case 'connect':
if (!ssid) {
return { content: [{ type: 'text', text: 'Connect requires ssid.' }], isError: true };
}
// Check if profile exists
if (password) {
// Create a temporary profile XML for new networks
ps = `
$profileXml = @"
<?xml version="1.0"?>
<WLANProfile xmlns="http://www.microsoft.com/networking/WLAN/profile/v1">
<name>${ssid}</name>
<SSIDConfig><SSID><name>${ssid}</name></SSID></SSIDConfig>
<connectionType>ESS</connectionType>
<connectionMode>auto</connectionMode>
<MSM><security>
<authEncryption><authentication>WPA2PSK</authentication><encryption>AES</encryption><useOneX>false</useOneX></authEncryption>
<sharedKey><keyType>passPhrase</keyType><protected>false</protected><keyMaterial>${password}</keyMaterial></sharedKey>
</security></MSM>
</WLANProfile>
"@
$tempFile = [IO.Path]::GetTempFileName()
$profileXml | Out-File -Encoding UTF8 $tempFile
netsh wlan add profile filename="$tempFile" 2>&1 | Out-Null
Remove-Item $tempFile
netsh wlan connect name="${ssid.replace(/"/g, '""')}" 2>&1
Start-Sleep -Seconds 3
$iface = netsh wlan show interfaces
$connectedSsid = ($iface | Select-String 'SSID\\s+:' | Select-Object -First 1) -replace '.*:\\s*',''
if ($connectedSsid.Trim() -eq '${ssid}') { "Connected to ${ssid}" } else { "Connection attempt sent for ${ssid}" }`;
} else {
ps = `netsh wlan connect name="${ssid.replace(/"/g, '""')}" 2>&1; Start-Sleep -Seconds 2; "Connect attempt sent for ${ssid}"`;
}
break;
}
const result = await runPowerShell(ps, { timeout: 20000 });
return {
content: [{ type: 'text', text: result.stdout || result.stderr }],
isError: result.exitCode !== 0,
};
},
);
}