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
- 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
158 lines
6.2 KiB
TypeScript
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,
|
|
};
|
|
},
|
|
);
|
|
}
|