Merge pull request 'feat: implement v2.0-v2.2 -- 19 new tools (63 total)' (#64) from dev into main
Generic: Repo Health / Access control (push) Successful in 3s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 11s
Universal: Changelog Validation / Validate CHANGELOG.md (push) Failing after 13s
MCP: Standards Compliance / Secret Scanning (push) Successful in 10s
MCP: Tool Inventory / inventory (push) Failing after 12s
MCP: Standards Compliance / License Header Validation (push) Failing after 11s
MCP: Standards Compliance / Repository Structure Validation (push) Failing after 8s
MCP: Standards Compliance / Coding Standards Check (push) Failing after 12s
MCP: Build & Validate / build (20) (push) Failing after 22s
MCP: Standards Compliance / Workflow Configuration Check (push) Failing after 11s
MCP: Build & Validate / build (22) (push) Failing after 24s
MCP: Standards Compliance / README Completeness Check (push) Failing after 9s
MCP: Standards Compliance / Documentation Quality Check (push) Successful in 12s
Universal: Build & Release / Build & Release Pipeline (push) Failing after 30s
MCP: Standards Compliance / File Naming Standards (push) Successful in 7s
MCP: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 7s
MCP: Standards Compliance / Git Repository Hygiene (push) Successful in 17s
MCP: Standards Compliance / Line Length Check (push) Failing after 10s
MCP: Standards Compliance / Script Integrity Validation (push) Successful in 14s
MCP: Standards Compliance / File Size Limits (push) Successful in 6s
MCP: Standards Compliance / Dead Code Detection (push) Successful in 11s
MCP: Standards Compliance / Binary File Detection (push) Successful in 7s
MCP: Build & Release / Build, Validate & Release (push) Failing after 41s
MCP: Standards Compliance / TODO/FIXME Tracking (push) Successful in 8s
MCP: Standards Compliance / Broken Link Detection (push) Successful in 5s
MCP: Standards Compliance / API Documentation Coverage (push) Successful in 6s
MCP: Standards Compliance / Accessibility Check (push) Successful in 8s
MCP: Standards Compliance / Performance Metrics (push) Successful in 7s
MCP: Standards Compliance / Terraform Configuration Validation (push) Successful in 14s
MCP: Standards Compliance / Unused Dependencies Check (push) Successful in 1m4s
MCP: Standards Compliance / Code Duplication Detection (push) Successful in 1m9s
MCP: Standards Compliance / Code Complexity Analysis (push) Successful in 1m9s
MCP: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 1m7s
MCP: Standards Compliance / Enterprise Readiness Check (push) Successful in 1m1s
MCP: Standards Compliance / Repository Health Check (push) Successful in 59s
MCP: Standards Compliance / Version Consistency Check (push) Successful in 1m18s
MCP: Standards Compliance / Compliance Summary (push) Failing after 1s
Universal: CodeQL Analysis / Analyze (javascript) (push) Failing after 1m54s
Universal: CodeQL Analysis / Analyze (actions) (push) Failing after 1m56s
Universal: CodeQL Analysis / Security Scan Summary (push) Successful in 1s
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

This commit was merged in pull request #64.
This commit is contained in:
2026-05-26 04:01:39 +00:00
10 changed files with 1361 additions and 1 deletions
+25 -1
View File
@@ -32,10 +32,19 @@ import { registerAppsTools } from './tools/apps.js';
import { registerDialogTools } from './tools/dialog.js';
import { registerNetstatTools } from './tools/netstat.js';
import { registerRecycleBinTools } from './tools/recycle_bin.js';
import { registerBluetoothTools } from './tools/bluetooth.js';
import { registerWifiTools } from './tools/wifi.js';
import { registerUsbTools } from './tools/usb.js';
import { registerPrinterTools } from './tools/printer.js';
import { registerHostsTools } from './tools/hosts.js';
import { registerThemeTools } from './tools/theme.js';
import { registerVirtualDesktopTools } from './tools/virtual_desktop.js';
import { registerFirewallTools } from './tools/firewall.js';
import { registerMaintenanceTools } from './tools/maintenance.js';
const server = new McpServer({
name: 'mcp_windows',
version: '1.0.0',
version: '2.0.0',
});
// v1.0 — Core
@@ -73,6 +82,21 @@ registerDialogTools(server);
registerNetstatTools(server);
registerRecycleBinTools(server);
// v2.0 — Connectivity & Hardware
registerBluetoothTools(server);
registerWifiTools(server);
registerUsbTools(server);
registerPrinterTools(server);
registerHostsTools(server);
// v2.1 — Appearance & Desktop
registerThemeTools(server);
registerVirtualDesktopTools(server);
// v2.2 — Security & Maintenance
registerFirewallTools(server);
registerMaintenanceTools(server);
async function main(): Promise<void> {
const transport = new StdioServerTransport();
await server.connect(transport);
+114
View File
@@ -0,0 +1,114 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tools: windows_bluetooth_get (#45), windows_bluetooth_control (#46)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerBluetoothTools(server: McpServer): void {
server.tool(
'windows_bluetooth_get',
'Get Bluetooth adapter status and list paired/connected devices.',
{},
async () => {
const ps = `
$adapter = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -match 'Bluetooth' -and $_.Class -eq 'Bluetooth' } | Select-Object -First 1
$enabled = if ($adapter) { $adapter.Status -eq 'OK' } else { $false }
$devices = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue |
Where-Object { $_.FriendlyName -and $_.Class -eq 'Bluetooth' -and $_.FriendlyName -notmatch 'Bluetooth|Radio|Enumerator' } |
ForEach-Object {
[PSCustomObject]@{
Name = $_.FriendlyName
Status = $_.Status
InstanceId = $_.InstanceId
Connected = $_.Status -eq 'OK'
}
}
[PSCustomObject]@{
AdapterPresent = $null -ne $adapter
AdapterName = if ($adapter) { $adapter.FriendlyName } else { 'None' }
Enabled = $enabled
Devices = $devices
} | ConvertTo-Json -Depth 4 -Compress`;
const result = await runPowerShell(ps, { timeout: 15000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const info = JSON.parse(result.stdout);
const lines: string[] = [
`Bluetooth: ${info.AdapterPresent ? (info.Enabled ? 'ON' : 'OFF') : 'Not available'}`,
`Adapter: ${info.AdapterName}`,
];
const devices = Array.isArray(info.Devices) ? info.Devices : info.Devices ? [info.Devices] : [];
if (devices.length > 0) {
lines.push('', 'Paired Devices:');
for (const d of devices) {
const icon = d.Connected ? '[CON]' : '[ ]';
lines.push(` ${icon} ${d.Name}`);
}
} else {
lines.push('', 'No paired devices.');
}
return { content: [{ type: 'text', text: lines.join('\n') }] };
},
);
server.tool(
'windows_bluetooth_control',
'Enable/disable Bluetooth adapter or disconnect a device.',
{
action: z.enum(['enable', 'disable', 'disconnect']).describe('Action'),
device: z.string().optional().describe('Device name (for disconnect)'),
},
async ({ action, device }) => {
let ps: string;
switch (action) {
case 'enable':
ps = `
$adapter = Get-PnpDevice -Class Bluetooth -ErrorAction Stop | Where-Object { $_.FriendlyName -match 'Bluetooth' -and $_.Class -eq 'Bluetooth' } | Select-Object -First 1
if ($adapter) {
Enable-PnpDevice -InstanceId $adapter.InstanceId -Confirm:$false -ErrorAction Stop
"Bluetooth enabled"
} else { "No Bluetooth adapter found" }`;
break;
case 'disable':
ps = `
$adapter = Get-PnpDevice -Class Bluetooth -ErrorAction Stop | Where-Object { $_.FriendlyName -match 'Bluetooth' -and $_.Class -eq 'Bluetooth' } | Select-Object -First 1
if ($adapter) {
Disable-PnpDevice -InstanceId $adapter.InstanceId -Confirm:$false -ErrorAction Stop
"Bluetooth disabled"
} else { "No Bluetooth adapter found" }`;
break;
case 'disconnect':
if (!device) {
return { content: [{ type: 'text', text: 'Disconnect requires device name.' }], isError: true };
}
ps = `
$dev = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -like '*${device.replace(/'/g, "''")}*' } | Select-Object -First 1
if ($dev) {
Disable-PnpDevice -InstanceId $dev.InstanceId -Confirm:$false -ErrorAction Stop
Start-Sleep -Seconds 1
Enable-PnpDevice -InstanceId $dev.InstanceId -Confirm:$false -ErrorAction Stop
"Disconnected and re-enabled: $($dev.FriendlyName)"
} else { "Device not found: ${device}" }`;
break;
}
const result = await runPowerShell(ps, { timeout: 15000 });
return {
content: [{ type: 'text', text: result.stdout || result.stderr }],
isError: result.exitCode !== 0,
};
},
);
}
+125
View File
@@ -0,0 +1,125 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tools: windows_firewall_get (#57), windows_firewall_manage (#58)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerFirewallTools(server: McpServer): void {
server.tool(
'windows_firewall_get',
'Get Windows Firewall status and list rules.',
{
filter: z.string().optional().describe('Filter rules by name (substring)'),
direction: z.enum(['inbound', 'outbound', 'all']).default('all').describe('Rule direction'),
enabled_only: z.boolean().default(true).describe('Only show enabled rules'),
limit: z.number().default(30).describe('Max rules to return'),
},
async ({ filter, direction, enabled_only, limit }) => {
const dirFilter = direction === 'inbound' ? "| Where-Object { \\$_.Direction -eq 'Inbound' }"
: direction === 'outbound' ? "| Where-Object { \\$_.Direction -eq 'Outbound' }" : '';
const nameFilter = filter ? `| Where-Object { \\$_.DisplayName -like '*${filter.replace(/'/g, "''")}*' }` : '';
const enabledFilter = enabled_only ? "| Where-Object { \\$_.Enabled -eq 'True' }" : '';
const ps = `
$profiles = Get-NetFirewallProfile -ErrorAction SilentlyContinue | ForEach-Object {
[PSCustomObject]@{ Name = $_.Name; Enabled = $_.Enabled; DefaultInbound = $_.DefaultInboundAction; DefaultOutbound = $_.DefaultOutboundAction }
}
$rules = Get-NetFirewallRule ${dirFilter} ${nameFilter} ${enabledFilter} -ErrorAction SilentlyContinue |
Select-Object -First ${limit} | ForEach-Object {
$port = ($_ | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)
[PSCustomObject]@{
Name = $_.DisplayName
Direction = $_.Direction.ToString()
Action = $_.Action.ToString()
Enabled = $_.Enabled.ToString()
Protocol = if ($port) { $port.Protocol } else { 'Any' }
Port = if ($port.LocalPort) { $port.LocalPort -join ',' } else { 'Any' }
Program = ($_ | Get-NetFirewallApplicationFilter -ErrorAction SilentlyContinue).Program
}
}
[PSCustomObject]@{
Profiles = $profiles
Rules = $rules
} | ConvertTo-Json -Depth 4 -Compress`;
const result = await runPowerShell(ps, { timeout: 30000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const data = JSON.parse(result.stdout);
const lines: string[] = ['Firewall Profiles:'];
const profiles = Array.isArray(data.Profiles) ? data.Profiles : [data.Profiles];
for (const p of profiles.filter(Boolean)) {
lines.push(` ${p.Name}: ${p.Enabled ? 'ON' : 'OFF'} (In: ${p.DefaultInbound}, Out: ${p.DefaultOutbound})`);
}
const rules = Array.isArray(data.Rules) ? data.Rules : data.Rules ? [data.Rules] : [];
if (rules.length > 0) {
lines.push('', `Rules (${rules.length}):`);
for (const r of rules) {
const dir = r.Direction === 'Inbound' ? 'IN ' : 'OUT';
const act = r.Action === 'Allow' ? 'ALLOW' : 'BLOCK';
lines.push(` [${dir}] [${act}] ${(r.Name || '').padEnd(35).slice(0, 35)} ${r.Protocol}/${r.Port}`);
}
}
return { content: [{ type: 'text', text: lines.join('\n') }] };
},
);
server.tool(
'windows_firewall_manage',
'Create, enable, disable, or delete a firewall rule.',
{
action: z.enum(['create', 'enable', 'disable', 'delete']).describe('Action'),
name: z.string().describe('Rule name'),
direction: z.enum(['inbound', 'outbound']).optional().describe('Direction (for create)'),
rule_action: z.enum(['allow', 'block']).optional().describe('Allow or block (for create)'),
port: z.string().optional().describe('Port number or range (for create)'),
protocol: z.enum(['TCP', 'UDP', 'Any']).optional().describe('Protocol (for create)'),
program: z.string().optional().describe('Program path (for create)'),
},
async ({ action, name, direction, rule_action, port, protocol, program }) => {
let ps: string;
switch (action) {
case 'create': {
if (!direction || !rule_action) {
return { content: [{ type: 'text', text: 'Create requires direction and rule_action.' }], isError: true };
}
const dir = direction === 'inbound' ? 'Inbound' : 'Outbound';
const act = rule_action === 'allow' ? 'Allow' : 'Block';
const parts = [`New-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -Direction ${dir} -Action ${act}`];
if (port) parts.push(`-LocalPort ${port}`);
if (protocol && protocol !== 'Any') parts.push(`-Protocol ${protocol}`);
if (program) parts.push(`-Program '${program.replace(/'/g, "''")}'`);
parts.push('-ErrorAction Stop');
ps = `${parts.join(' ')} | Select-Object DisplayName,Direction,Action,Enabled | ConvertTo-Json -Compress`;
break;
}
case 'enable':
ps = `Enable-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Enabled: ${name}"`;
break;
case 'disable':
ps = `Disable-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Disabled: ${name}"`;
break;
case 'delete':
ps = `Remove-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Deleted: ${name}"`;
break;
}
const result = await runPowerShell(ps, { timeout: 15000 });
return {
content: [{ type: 'text', text: result.stdout || result.stderr }],
isError: result.exitCode !== 0,
};
},
);
}
+135
View File
@@ -0,0 +1,135 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tool: windows_hosts_file (#51)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { readFile, writeFile, copyFile } from 'node:fs/promises';
const HOSTS_PATH = 'C:\\Windows\\System32\\drivers\\etc\\hosts';
export function registerHostsTools(server: McpServer): void {
server.tool(
'windows_hosts_file',
'Read and manage the Windows hosts file. List, add, remove, or toggle entries.',
{
action: z.enum(['list', 'add', 'remove', 'enable', 'disable']).default('list').describe('Action'),
ip: z.string().optional().describe('IP address (for add)'),
hostname: z.string().optional().describe('Hostname (for add/remove/enable/disable)'),
comment: z.string().optional().describe('Comment (for add)'),
},
async ({ action, ip, hostname, comment }) => {
try {
const content = await readFile(HOSTS_PATH, 'utf-8');
const lines = content.split(/\r?\n/);
if (action === 'list') {
const entries: Array<{ line: number; enabled: boolean; ip: string; host: string; comment: string }> = [];
lines.forEach((line, i) => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#') && !trimmed.match(/^#\s*\d/)) {
// Check if it's a commented-out entry
const commented = trimmed.replace(/^#\s*/, '');
const parts = commented.split(/\s+/);
if (parts.length >= 2 && parts[0].match(/^\d+\.\d+\.\d+\.\d+$|^[a-f0-9:]+$/i)) {
entries.push({
line: i + 1,
enabled: false,
ip: parts[0],
host: parts[1],
comment: parts.slice(2).join(' ').replace(/^#\s*/, ''),
});
}
return;
}
const parts = trimmed.split(/\s+/);
if (parts.length >= 2 && parts[0].match(/^\d+\.\d+\.\d+\.\d+$|^[a-f0-9:]+$/i)) {
entries.push({
line: i + 1,
enabled: true,
ip: parts[0],
host: parts[1],
comment: parts.slice(2).join(' ').replace(/^#\s*/, ''),
});
}
});
const output = entries.map(e => {
const status = e.enabled ? '[ON] ' : '[OFF]';
return `${status} ${e.ip.padEnd(18)} ${e.host.padEnd(35)} ${e.comment}`;
});
const header = `State ${'IP'.padEnd(18)} ${'Hostname'.padEnd(35)} Comment`;
return {
content: [{ type: 'text', text: `${HOSTS_PATH}\n${header}\n${'─'.repeat(80)}\n${output.join('\n')}\n\n${entries.length} entries` }],
};
}
// Backup before modification
await copyFile(HOSTS_PATH, HOSTS_PATH + '.bak');
if (action === 'add') {
if (!ip || !hostname) {
return { content: [{ type: 'text', text: 'Add requires ip and hostname.' }], isError: true };
}
const entry = comment ? `${ip}\t${hostname}\t# ${comment}` : `${ip}\t${hostname}`;
const newContent = content.trimEnd() + '\n' + entry + '\n';
await writeFile(HOSTS_PATH, newContent, 'utf-8');
return { content: [{ type: 'text', text: `Added: ${ip} ${hostname}` }] };
}
if (action === 'remove') {
if (!hostname) {
return { content: [{ type: 'text', text: 'Remove requires hostname.' }], isError: true };
}
const filtered = lines.filter(line => {
const parts = line.trim().replace(/^#\s*/, '').split(/\s+/);
return !(parts.length >= 2 && parts[1] === hostname);
});
await writeFile(HOSTS_PATH, filtered.join('\n'), 'utf-8');
return { content: [{ type: 'text', text: `Removed entries for: ${hostname}` }] };
}
if (action === 'disable') {
if (!hostname) {
return { content: [{ type: 'text', text: 'Disable requires hostname.' }], isError: true };
}
const updated = lines.map(line => {
const parts = line.trim().split(/\s+/);
if (parts.length >= 2 && parts[1] === hostname && !line.trim().startsWith('#')) {
return '# ' + line;
}
return line;
});
await writeFile(HOSTS_PATH, updated.join('\n'), 'utf-8');
return { content: [{ type: 'text', text: `Disabled: ${hostname}` }] };
}
if (action === 'enable') {
if (!hostname) {
return { content: [{ type: 'text', text: 'Enable requires hostname.' }], isError: true };
}
const updated = lines.map(line => {
const trimmed = line.trim();
if (trimmed.startsWith('#')) {
const uncommented = trimmed.replace(/^#\s*/, '');
const parts = uncommented.split(/\s+/);
if (parts.length >= 2 && parts[1] === hostname) {
return uncommented;
}
}
return line;
});
await writeFile(HOSTS_PATH, updated.join('\n'), 'utf-8');
return { content: [{ type: 'text', text: `Enabled: ${hostname}` }] };
}
return { content: [{ type: 'text', text: 'Unknown action.' }], isError: true };
} catch (err) {
return { content: [{ type: 'text', text: `Error: ${err}. Hosts file modification may require elevation.` }], isError: true };
}
},
);
}
+299
View File
@@ -0,0 +1,299 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tools: windows_updates (#59), windows_event_log (#60),
* windows_restore_point (#61), windows_certificate_list (#62),
* windows_performance_monitor (#63)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerMaintenanceTools(server: McpServer): void {
server.tool(
'windows_updates',
'Check Windows Update status, pending updates, and recent history.',
{
action: z.enum(['status', 'history']).default('status').describe('Check status or view history'),
limit: z.number().default(20).describe('Max history entries'),
},
async ({ action, limit }) => {
if (action === 'history') {
const ps = `
$session = New-Object -ComObject Microsoft.Update.Session
$searcher = $session.CreateUpdateSearcher()
$count = $searcher.GetTotalHistoryCount()
$history = $searcher.QueryHistory(0, [math]::Min($count, ${limit}))
$history | ForEach-Object {
[PSCustomObject]@{
Date = $_.Date.ToString('yyyy-MM-dd HH:mm')
Title = $_.Title
Result = switch ($_.ResultCode) { 0 {'Not Started'} 1 {'In Progress'} 2 {'Succeeded'} 3 {'Succeeded with Errors'} 4 {'Failed'} 5 {'Aborted'} default {'Unknown'} }
}
} | ConvertTo-Json -Depth 3 -Compress`;
const result = await runPowerShell(ps, { timeout: 30000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
if (!result.stdout) {
return { content: [{ type: 'text', text: 'No update history.' }] };
}
const entries = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = entries.map((e: { Date: string; Title: string; Result: string }) =>
`${e.Result.padEnd(10)} ${e.Date} ${(e.Title || '').slice(0, 70)}`,
);
return { content: [{ type: 'text', text: `Recent updates:\n${lines.join('\n')}` }] };
}
// Status
const ps = `
$reboot = Test-Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired'
$lastCheck = (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\Results\\Detect' -Name LastSuccessTime -ErrorAction SilentlyContinue).LastSuccessTime
[PSCustomObject]@{
RestartPending = $reboot
LastCheckTime = $lastCheck
} | ConvertTo-Json -Compress`;
const result = await runPowerShell(ps, { timeout: 15000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const info = JSON.parse(result.stdout);
return {
content: [{
type: 'text',
text: `Windows Update:\n Restart pending: ${info.RestartPending}\n Last check: ${info.LastCheckTime || 'Unknown'}`,
}],
};
},
);
server.tool(
'windows_event_log',
'Read Windows Event Log entries. Filter by log, level, source, or time.',
{
log: z.string().default('System').describe('Log name (System, Application, Security, etc.)'),
level: z.enum(['Critical', 'Error', 'Warning', 'Information', 'All']).default('All').describe('Event level'),
source: z.string().optional().describe('Event source filter'),
limit: z.number().default(20).describe('Max entries'),
hours: z.number().optional().describe('Only events from last N hours'),
},
async ({ log, level, source, limit, hours }) => {
const levelMap: Record<string, string> = {
Critical: '1', Error: '2', Warning: '3', Information: '4',
};
const levelFilter = level !== 'All' ? `-Level ${levelMap[level]}` : '';
const sourceFilter = source ? `-ProviderName '${source.replace(/'/g, "''")}'` : '';
const timeFilter = hours ? `-After (Get-Date).AddHours(-${hours})` : '';
const ps = `
Get-WinEvent -LogName '${log.replace(/'/g, "''")}' -MaxEvents ${limit} ${levelFilter ? `| Where-Object { $_.Level -eq ${levelMap[level]} }` : ''} -ErrorAction SilentlyContinue |
${source ? `Where-Object { $_.ProviderName -like '*${source.replace(/'/g, "''")}*' } |` : ''}
${hours ? `Where-Object { $_.TimeCreated -gt (Get-Date).AddHours(-${hours}) } |` : ''}
Select-Object -First ${limit} |
ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
Level = $_.LevelDisplayName
Source = $_.ProviderName
EventId = $_.Id
Message = ($_.Message -split [char]10)[0].Substring(0, [math]::Min(($_.Message -split [char]10)[0].Length, 100))
}
} | ConvertTo-Json -Depth 3 -Compress`;
const result = await runPowerShell(ps, { timeout: 20000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
if (!result.stdout) {
return { content: [{ type: 'text', text: 'No events found.' }] };
}
const events = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = events.map((e: { Time: string; Level: string; Source: string; EventId: number; Message: string }) =>
`${(e.Level || '').padEnd(12)} ${e.Time} ${String(e.EventId).padStart(5)} ${(e.Source || '').padEnd(25).slice(0, 25)} ${(e.Message || '').slice(0, 50)}`,
);
const header = `${'Level'.padEnd(12)} ${'Time'.padEnd(19)} ${'ID'.padStart(5)} ${'Source'.padEnd(25)} Message`;
return { content: [{ type: 'text', text: `${log} log:\n${header}\n${'─'.repeat(120)}\n${lines.join('\n')}` }] };
},
);
server.tool(
'windows_restore_point',
'List or create System Restore points.',
{
action: z.enum(['list', 'create']).default('list').describe('Action'),
description: z.string().optional().describe('Description for new restore point'),
},
async ({ action, description }) => {
if (action === 'create') {
const desc = description || 'mcp_windows restore point';
const ps = `Checkpoint-Computer -Description '${desc.replace(/'/g, "''")}' -RestorePointType MODIFY_SETTINGS -ErrorAction Stop; "Restore point created: ${desc}"`;
const result = await runPowerShell(ps, { timeout: 60000 });
return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
}
const ps = `
try {
$points = Get-ComputerRestorePoint -ErrorAction Stop
if (-not $points) { Write-Output '[]'; return }
$points | ForEach-Object {
[PSCustomObject]@{
SequenceNumber = $_.SequenceNumber
Description = $_.Description
Type = switch ($_.RestorePointType) { 0 {'Application Install'} 1 {'Application Uninstall'} 10 {'Device Install'} 12 {'Modify Settings'} 13 {'Cancel'} default {'Other'} }
Date = $_.ConvertToDateTime($_.CreationTime).ToString('yyyy-MM-dd HH:mm')
}
} | ConvertTo-Json -Depth 3 -Compress
} catch {
Write-Output "RESTORE_ERROR:$($_.Exception.Message)"
}`;
const result = await runPowerShell(ps, { timeout: 15000 });
if (result.stdout?.startsWith('RESTORE_ERROR:')) {
const msg = result.stdout.replace('RESTORE_ERROR:', '');
return { content: [{ type: 'text', text: `System Restore: ${msg || 'Requires elevation or System Restore is disabled.'}` }] };
}
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr || 'Requires elevation.'}` }], isError: true };
}
if (!result.stdout || result.stdout === '[]') {
return { content: [{ type: 'text', text: 'No restore points found (System Restore may be disabled or requires elevation).' }] };
}
const points = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = points.map((p: { SequenceNumber: number; Description: string; Type: string; Date: string }) =>
` #${p.SequenceNumber} ${p.Date} ${(p.Type || '').padEnd(20)} ${p.Description}`,
);
return { content: [{ type: 'text', text: `System Restore points:\n${lines.join('\n')}` }] };
},
);
server.tool(
'windows_certificate_list',
'List certificates in the Windows certificate store.',
{
store: z.string().default('Cert:\\CurrentUser\\My').describe('Certificate store path'),
expiring_days: z.number().optional().describe('Only show certs expiring within N days'),
filter: z.string().optional().describe('Filter by subject (substring)'),
},
async ({ store, expiring_days, filter }) => {
const expiryFilter = expiring_days
? `| Where-Object { $_.NotAfter -lt (Get-Date).AddDays(${expiring_days}) -and $_.NotAfter -gt (Get-Date) }`
: '';
const nameFilter = filter
? `| Where-Object { $_.Subject -like '*${filter.replace(/'/g, "''")}*' }`
: '';
// Map store path to .NET enums
// Cert:\CurrentUser\My -> CurrentUser, My
const storeMatch = store.match(/Cert:\\\\?(CurrentUser|LocalMachine)\\\\?(\w+)/i);
const storeLocation = storeMatch ? storeMatch[1] : 'CurrentUser';
const storeName = storeMatch ? storeMatch[2] : 'My';
const ps = `
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('${storeName}', '${storeLocation}')
$store.Open('ReadOnly')
$certs = $store.Certificates
${expiring_days ? `$certs = $certs | Where-Object { $_.NotAfter -lt (Get-Date).AddDays(${expiring_days}) -and $_.NotAfter -gt (Get-Date) }` : ''}
${filter ? `$certs = $certs | Where-Object { $_.Subject -like '*${filter.replace(/'/g, "''")}*' }` : ''}
$certs | ForEach-Object {
[PSCustomObject]@{
Subject = $_.Subject
Issuer = $_.Issuer
Thumbprint = $_.Thumbprint
Expires = $_.NotAfter.ToString('yyyy-MM-dd')
KeyUsage = ($_.Extensions | Where-Object { $_ -is [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension] } | ForEach-Object { ($_.EnhancedKeyUsages | ForEach-Object { $_.FriendlyName }) -join ', ' })
}
} | ConvertTo-Json -Depth 3 -Compress
$store.Close()`;
const result = await runPowerShell(ps, { timeout: 15000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
if (!result.stdout) {
return { content: [{ type: 'text', text: 'No certificates found.' }] };
}
const certs = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = certs.map((c: { Subject: string; Expires: string; Thumbprint: string; KeyUsage: string }) =>
` ${c.Expires} ${(c.Thumbprint || '').slice(0, 16)}... ${(c.Subject || '').slice(0, 60)}`,
);
const header = ` ${'Expires'.padEnd(10)} ${'Thumbprint'.padEnd(19)} Subject`;
return { content: [{ type: 'text', text: `${store}\n${header}\n${'─'.repeat(90)}\n${lines.join('\n')}\n\n${certs.length} certificates` }] };
},
);
server.tool(
'windows_performance_monitor',
'Get real-time system performance: CPU per core, RAM, disk I/O, network throughput, top processes.',
{},
async () => {
const ps = `
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$os = Get-CimInstance Win32_OperatingSystem
$cs = Get-CimInstance Win32_ComputerSystem
$perfDisk = Get-CimInstance Win32_PerfFormattedData_PerfDisk_LogicalDisk -Filter "Name='_Total'" -ErrorAction SilentlyContinue
$perfNet = Get-CimInstance Win32_PerfFormattedData_Tcpip_NetworkInterface -ErrorAction SilentlyContinue | Select-Object -First 1
$topCPU = Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 | ForEach-Object {
[PSCustomObject]@{ Name = $_.ProcessName; PID = $_.Id; CPU = [math]::Round($_.CPU, 1); MemMB = [math]::Round($_.WorkingSet64 / 1MB, 1) }
}
$topMem = Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 5 | ForEach-Object {
[PSCustomObject]@{ Name = $_.ProcessName; PID = $_.Id; MemMB = [math]::Round($_.WorkingSet64 / 1MB, 1) }
}
[PSCustomObject]@{
CPUUsage = "$([math]::Round($cpu.LoadPercentage, 0))%"
CPUCores = $cpu.NumberOfLogicalProcessors
RAMTotalGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
RAMUsedGB = [math]::Round(($cs.TotalPhysicalMemory - $os.FreePhysicalMemory * 1KB) / 1GB, 1)
RAMUsedPct = [math]::Round(($cs.TotalPhysicalMemory - $os.FreePhysicalMemory * 1KB) / $cs.TotalPhysicalMemory * 100, 1)
DiskReadBps = if ($perfDisk) { "$([math]::Round($perfDisk.DiskReadBytesPersec / 1MB, 2)) MB/s" } else { 'N/A' }
DiskWriteBps = if ($perfDisk) { "$([math]::Round($perfDisk.DiskWriteBytesPersec / 1MB, 2)) MB/s" } else { 'N/A' }
NetSentBps = if ($perfNet) { "$([math]::Round($perfNet.BytesSentPersec / 1MB, 2)) MB/s" } else { 'N/A' }
NetRecvBps = if ($perfNet) { "$([math]::Round($perfNet.BytesReceivedPersec / 1MB, 2)) MB/s" } else { 'N/A' }
TopCPU = $topCPU
TopMem = $topMem
} | ConvertTo-Json -Depth 4 -Compress`;
const result = await runPowerShell(ps, { timeout: 30000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const p = JSON.parse(result.stdout);
const topCpu = Array.isArray(p.TopCPU) ? p.TopCPU : [p.TopCPU].filter(Boolean);
const topMem = Array.isArray(p.TopMem) ? p.TopMem : [p.TopMem].filter(Boolean);
return {
content: [{
type: 'text',
text: [
`CPU: ${p.CPUUsage} (${p.CPUCores} threads)`,
`RAM: ${p.RAMUsedGB}/${p.RAMTotalGB} GB (${p.RAMUsedPct}%)`,
`Disk: Read ${p.DiskReadBps} / Write ${p.DiskWriteBps}`,
`Net: Send ${p.NetSentBps} / Recv ${p.NetRecvBps}`,
'',
'Top by CPU:',
...topCpu.map((t: { Name: string; PID: number; CPU: number; MemMB: number }) =>
` ${String(t.PID).padStart(6)} ${t.Name.padEnd(20)} ${String(t.CPU).padStart(8)}s ${String(t.MemMB).padStart(8)} MB`,
),
'',
'Top by Memory:',
...topMem.map((t: { Name: string; PID: number; MemMB: number }) =>
` ${String(t.PID).padStart(6)} ${t.Name.padEnd(20)} ${String(t.MemMB).padStart(8)} MB`,
),
].join('\n'),
}],
};
},
);
}
+114
View File
@@ -0,0 +1,114 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tool: windows_printer_list (#50)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerPrinterTools(server: McpServer): void {
server.tool(
'windows_printer_list',
'List printers, show print queues, set default, or clear a queue.',
{
action: z.enum(['list', 'queue', 'set_default', 'clear_queue']).default('list').describe('Action'),
printer: z.string().optional().describe('Printer name (for queue/set_default/clear_queue)'),
},
async ({ action, printer }) => {
switch (action) {
case 'list': {
const ps = `
Get-Printer -ErrorAction SilentlyContinue | ForEach-Object {
$default = if ((Get-CimInstance Win32_Printer -Filter "Name='$($_.Name.Replace("'","''"))'" -ErrorAction SilentlyContinue).Default) { $true } else { $false }
[PSCustomObject]@{
Name = $_.Name
Status = $_.PrinterStatus
Type = $_.Type
Port = $_.PortName
Driver = $_.DriverName
Default = $default
Shared = $_.Shared
}
} | ConvertTo-Json -Depth 3 -Compress`;
const result = await runPowerShell(ps, { timeout: 45000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
if (!result.stdout) {
return { content: [{ type: 'text', text: 'No printers found.' }] };
}
const printers = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = printers.map((p: { Name: string; Status: string; Driver: string; Port: string; Default: boolean }) => {
const def = p.Default ? ' *' : ' ';
return `${def} ${(p.Name || '').padEnd(35).slice(0, 35)} ${(p.Driver || '').padEnd(25).slice(0, 25)} ${p.Port || ''}`;
});
const header = ` ${'Name'.padEnd(35)} ${'Driver'.padEnd(25)} Port`;
return {
content: [{ type: 'text', text: `${header}\n${'─'.repeat(80)}\n${lines.join('\n')}\n\n${printers.length} printers (* = default)` }],
};
}
case 'queue': {
if (!printer) {
return { content: [{ type: 'text', text: 'Queue requires printer name.' }], isError: true };
}
const ps = `
Get-PrintJob -PrinterName '${printer.replace(/'/g, "''")}' -ErrorAction Stop | ForEach-Object {
[PSCustomObject]@{
Id = $_.Id
Document = $_.DocumentName
Status = $_.JobStatus
Pages = $_.TotalPages
Size = "$([math]::Round($_.Size / 1KB, 1)) KB"
Submitted = $_.SubmittedTime.ToString('yyyy-MM-dd HH:mm')
}
} | ConvertTo-Json -Depth 3 -Compress`;
const result = await runPowerShell(ps, { timeout: 10000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
if (!result.stdout) {
return { content: [{ type: 'text', text: `Print queue for "${printer}" is empty.` }] };
}
const jobs = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = jobs.map((j: { Id: number; Document: string; Status: string; Pages: number; Size: string }) =>
` #${j.Id} ${(j.Document || '').padEnd(30).slice(0, 30)} ${(j.Status || '').padEnd(12)} ${j.Pages} pg ${j.Size}`,
);
return { content: [{ type: 'text', text: `Queue for "${printer}":\n${lines.join('\n')}` }] };
}
case 'set_default': {
if (!printer) {
return { content: [{ type: 'text', text: 'set_default requires printer name.' }], isError: true };
}
const ps = `
$p = Get-CimInstance Win32_Printer -Filter "Name='${printer.replace(/'/g, "''")}'" -ErrorAction Stop
Invoke-CimMethod -InputObject $p -MethodName SetDefaultPrinter | Out-Null
"Default printer set to: ${printer}"`;
const result = await runPowerShell(ps, { timeout: 10000 });
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
}
case 'clear_queue': {
if (!printer) {
return { content: [{ type: 'text', text: 'clear_queue requires printer name.' }], isError: true };
}
const ps = `
$jobs = Get-PrintJob -PrinterName '${printer.replace(/'/g, "''")}' -ErrorAction Stop
$count = @($jobs).Count
$jobs | Remove-PrintJob -ErrorAction SilentlyContinue
"Cleared $count job(s) from ${printer}"`;
const result = await runPowerShell(ps, { timeout: 10000 });
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
}
}
},
);
}
+205
View File
@@ -0,0 +1,205 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tools: windows_theme_get (#52), windows_theme_set (#53),
* windows_focus_mode (#55), windows_default_apps (#56)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerThemeTools(server: McpServer): void {
server.tool(
'windows_theme_get',
'Get current Windows theme: dark/light mode, accent color, wallpaper, transparency, taskbar alignment.',
{},
async () => {
const ps = `
$personalize = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize'
$accent = 'HKCU:\\Software\\Microsoft\\Windows\\DWM'
$taskbar = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced'
$wallpaper = (Get-ItemProperty -Path 'HKCU:\\Control Panel\\Desktop' -Name Wallpaper -ErrorAction SilentlyContinue).Wallpaper
$appsDark = (Get-ItemProperty -Path $personalize -Name AppsUseLightTheme -ErrorAction SilentlyContinue).AppsUseLightTheme -eq 0
$systemDark = (Get-ItemProperty -Path $personalize -Name SystemUsesLightTheme -ErrorAction SilentlyContinue).SystemUsesLightTheme -eq 0
$transparency = (Get-ItemProperty -Path $personalize -Name EnableTransparency -ErrorAction SilentlyContinue).EnableTransparency -eq 1
$accentColor = (Get-ItemProperty -Path $accent -Name AccentColor -ErrorAction SilentlyContinue).AccentColor
$taskbarAlign = (Get-ItemProperty -Path $taskbar -Name TaskbarAl -ErrorAction SilentlyContinue).TaskbarAl
$accentHex = if ($accentColor) {
$b = ($accentColor -band 0xFF0000) -shr 16
$g = ($accentColor -band 0x00FF00) -shr 8
$r = ($accentColor -band 0x0000FF)
'#{0:X2}{1:X2}{2:X2}' -f $r, $g, $b
} else { 'Unknown' }
[PSCustomObject]@{
AppsDarkMode = $appsDark
SystemDarkMode = $systemDark
AccentColor = $accentHex
Transparency = $transparency
Wallpaper = $wallpaper
TaskbarAlignment = if ($taskbarAlign -eq 0) { 'Left' } else { 'Center' }
} | ConvertTo-Json -Compress`;
const result = await runPowerShell(ps, { timeout: 10000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const t = JSON.parse(result.stdout);
return {
content: [{
type: 'text',
text: [
`Dark mode: Apps=${t.AppsDarkMode}, System=${t.SystemDarkMode}`,
`Accent color: ${t.AccentColor}`,
`Transparency: ${t.Transparency ? 'On' : 'Off'}`,
`Taskbar: ${t.TaskbarAlignment}`,
`Wallpaper: ${t.Wallpaper || '(none)'}`,
].join('\n'),
}],
};
},
);
server.tool(
'windows_theme_set',
'Set dark/light mode, accent color, wallpaper, transparency, or taskbar alignment.',
{
dark_mode: z.enum(['on', 'off']).optional().describe('Set dark mode for apps and system'),
wallpaper: z.string().optional().describe('Wallpaper file path'),
wallpaper_fit: z.enum(['fill', 'fit', 'stretch', 'tile', 'center', 'span']).optional().describe('Wallpaper fit mode'),
transparency: z.enum(['on', 'off']).optional().describe('Transparency effects'),
taskbar_align: z.enum(['left', 'center']).optional().describe('Taskbar alignment'),
},
async ({ dark_mode, wallpaper, wallpaper_fit, transparency, taskbar_align }) => {
const commands: string[] = [];
if (dark_mode) {
const val = dark_mode === 'on' ? 0 : 1;
commands.push(`Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize' -Name AppsUseLightTheme -Value ${val}`);
commands.push(`Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize' -Name SystemUsesLightTheme -Value ${val}`);
commands.push(`"Dark mode: ${dark_mode}"`);
}
if (transparency) {
const val = transparency === 'on' ? 1 : 0;
commands.push(`Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize' -Name EnableTransparency -Value ${val}`);
commands.push(`"Transparency: ${transparency}"`);
}
if (taskbar_align) {
const val = taskbar_align === 'left' ? 0 : 1;
commands.push(`Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced' -Name TaskbarAl -Value ${val}`);
commands.push(`"Taskbar: ${taskbar_align}"`);
}
if (wallpaper) {
const fitMap: Record<string, string> = { fill: '10', fit: '6', stretch: '2', tile: '0', center: '0', span: '22' };
const fit = wallpaper_fit || 'fill';
commands.push(`
Set-ItemProperty -Path 'HKCU:\\Control Panel\\Desktop' -Name WallpaperStyle -Value '${fitMap[fit]}'
Set-ItemProperty -Path 'HKCU:\\Control Panel\\Desktop' -Name TileWallpaper -Value '${fit === 'tile' ? '1' : '0'}'
Add-Type -TypeDefinition 'using System.Runtime.InteropServices; public class Wallpaper { [DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); }'
[Wallpaper]::SystemParametersInfo(0x0014, 0, '${wallpaper.replace(/'/g, "''")}', 0x01 -bor 0x02) | Out-Null
"Wallpaper set: ${wallpaper} (${fit})"
`);
}
if (commands.length === 0) {
return { content: [{ type: 'text', text: 'No changes specified.' }], isError: true };
}
const result = await runPowerShell(commands.join('\n'), { timeout: 15000 });
return {
content: [{ type: 'text', text: result.stdout || result.stderr }],
isError: result.exitCode !== 0,
};
},
);
server.tool(
'windows_focus_mode',
'Get or set Windows Focus Assist / Do Not Disturb mode.',
{
action: z.enum(['get', 'priority', 'alarms', 'off']).default('get').describe('Get status or set mode'),
},
async ({ action }) => {
if (action === 'get') {
const ps = `
$regPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.notifications.quiethourssettings\\windows.data.notifications.quiethourssettings'
$mode = 'Unknown'
try {
$val = (Get-ItemProperty -Path $regPath -ErrorAction Stop).Data
if ($val) {
# The focus assist state is encoded in the binary blob
$mode = 'Check via Settings app (binary registry format)'
}
} catch { $mode = 'Off (or unable to read)' }
"Focus Assist: $mode"`;
const result = await runPowerShell(ps, { timeout: 10000 });
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
}
// Setting focus assist requires ms-settings URI
const ps = `Start-Process 'ms-settings:quiethours'; "Opened Focus Assist settings. Mode requested: ${action}"`;
const result = await runPowerShell(ps, { timeout: 10000 });
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
},
);
server.tool(
'windows_default_apps',
'Get default applications or open the default apps settings page.',
{
action: z.enum(['get', 'open_settings']).default('get').describe('Get defaults or open settings'),
extension: z.string().optional().describe('File extension to check (e.g. ".pdf")'),
},
async ({ action, extension }) => {
if (action === 'open_settings') {
await runPowerShell('Start-Process "ms-settings:defaultapps"');
return { content: [{ type: 'text', text: 'Opened Default Apps settings.' }] };
}
if (extension) {
const ps = `
$assoc = cmd /c assoc ${extension} 2>$null
$ftype = if ($assoc) { $type = ($assoc -split '=')[1]; cmd /c ftype $type 2>$null } else { $null }
[PSCustomObject]@{
Extension = '${extension}'
FileType = if ($assoc) { ($assoc -split '=')[1] } else { 'Not associated' }
OpensWith = if ($ftype) { ($ftype -split '=')[1] } else { 'Unknown' }
} | ConvertTo-Json -Compress`;
const result = await runPowerShell(ps, { timeout: 10000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const info = JSON.parse(result.stdout);
return { content: [{ type: 'text', text: `${info.Extension} -> ${info.FileType} -> ${info.OpensWith}` }] };
}
// List common defaults
const ps = `
$defaults = @('.html','.pdf','.txt','.jpg','.png','.mp3','.mp4','.zip') | ForEach-Object {
$ext = $_
$assoc = cmd /c assoc $ext 2>$null
$type = if ($assoc) { ($assoc -split '=')[1] } else { 'N/A' }
[PSCustomObject]@{ Extension = $ext; FileType = $type }
}
$defaults | ConvertTo-Json -Depth 3 -Compress`;
const result = await runPowerShell(ps, { timeout: 15000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const defaults = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = defaults.map((d: { Extension: string; FileType: string }) =>
` ${d.Extension.padEnd(8)} ${d.FileType}`,
);
return { content: [{ type: 'text', text: `Default file associations:\n${lines.join('\n')}` }] };
},
);
}
+65
View File
@@ -0,0 +1,65 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tool: windows_usb_devices (#49)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerUsbTools(server: McpServer): void {
server.tool(
'windows_usb_devices',
'List connected USB devices with name, manufacturer, type, and status. Supports safe eject for storage.',
{
eject: z.string().optional().describe('Drive letter to safely eject (e.g. "E:")'),
},
async ({ eject }) => {
if (eject) {
const letter = eject.replace(':', '').toUpperCase();
const ps = `
$vol = Get-CimInstance Win32_Volume -Filter "DriveLetter='${letter}:'" -ErrorAction Stop
$ejectResult = $vol | Invoke-CimMethod -MethodName Dismount -Arguments @{Force=$false} -ErrorAction Stop
if ($ejectResult.ReturnValue -eq 0) { "Safely ejected ${letter}:" } else { "Eject failed (code: $($ejectResult.ReturnValue)). Close all files on the drive first." }`;
const result = await runPowerShell(ps, { timeout: 15000 });
return {
content: [{ type: 'text', text: result.stdout || result.stderr }],
isError: result.exitCode !== 0,
};
}
const ps = `
Get-PnpDevice -Class USB -PresentOnly -ErrorAction SilentlyContinue | ForEach-Object {
[PSCustomObject]@{
Name = $_.FriendlyName
Status = $_.Status
Manufacturer = $_.Manufacturer
InstanceId = $_.InstanceId
Class = $_.Class
}
} | Where-Object { $_.Name } | Sort-Object Name | ConvertTo-Json -Depth 3 -Compress`;
const result = await runPowerShell(ps, { timeout: 15000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
if (!result.stdout) {
return { content: [{ type: 'text', text: 'No USB devices found.' }] };
}
const devices = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = devices.map((d: { Name: string; Status: string; Manufacturer: string }) => {
const status = d.Status === 'OK' ? '[OK]' : `[${d.Status?.slice(0, 3) || '?'}]`;
return `${status} ${(d.Name || '').padEnd(45).slice(0, 45)} ${(d.Manufacturer || '').slice(0, 25)}`;
});
const header = `Sta ${'Name'.padEnd(45)} Manufacturer`;
return {
content: [{ type: 'text', text: `${header}\n${'─'.repeat(80)}\n${lines.join('\n')}\n\n${devices.length} USB devices` }],
};
},
);
}
+122
View File
@@ -0,0 +1,122 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tool: windows_virtual_desktop (#54)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerVirtualDesktopTools(server: McpServer): void {
server.tool(
'windows_virtual_desktop',
'List, create, switch, or remove virtual desktops.',
{
action: z.enum(['list', 'create', 'switch', 'remove']).default('list').describe('Action'),
index: z.number().optional().describe('Desktop index (0-based, for switch/remove)'),
},
async ({ action, index }) => {
switch (action) {
case 'list': {
const ps = `
$desktops = Get-CimInstance -Namespace root\\cimv2 -ClassName Win32_Desktop -ErrorAction SilentlyContinue
# Virtual desktops are best accessed via COM, but we can detect count from registry
$vdKey = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VirtualDesktops'
$ids = (Get-ItemProperty -Path $vdKey -Name VirtualDesktopIDs -ErrorAction SilentlyContinue).VirtualDesktopIDs
$currentId = (Get-ItemProperty -Path $vdKey -Name CurrentVirtualDesktop -ErrorAction SilentlyContinue).CurrentVirtualDesktop
$count = if ($ids) { $ids.Length / 16 } else { 1 }
[PSCustomObject]@{
Count = $count
CurrentIndex = 'Use Ctrl+Win+Left/Right to navigate'
Note = 'Windows does not expose virtual desktop names via public API'
} | ConvertTo-Json -Compress`;
const result = await runPowerShell(ps, { timeout: 10000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const info = JSON.parse(result.stdout);
return { content: [{ type: 'text', text: `Virtual desktops: ${info.Count}\n${info.Note}` }] };
}
case 'create': {
// Use keyboard shortcut via SendKeys
const ps = `
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class VDKeys {
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
public static void NewDesktop() {
// Ctrl+Win+D
keybd_event(0x11, 0, 0, UIntPtr.Zero); // Ctrl down
keybd_event(0x5B, 0, 0, UIntPtr.Zero); // Win down
keybd_event(0x44, 0, 0, UIntPtr.Zero); // D down
keybd_event(0x44, 0, 2, UIntPtr.Zero); // D up
keybd_event(0x5B, 0, 2, UIntPtr.Zero); // Win up
keybd_event(0x11, 0, 2, UIntPtr.Zero); // Ctrl up
}
}
'@
[VDKeys]::NewDesktop()
Start-Sleep -Milliseconds 500
"New virtual desktop created (switched to it)"`;
const result = await runPowerShell(ps, { timeout: 10000 });
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
}
case 'switch': {
if (index === undefined) {
return { content: [{ type: 'text', text: 'Switch requires index.' }], isError: true };
}
// Navigate left/right to reach target
const ps = `
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class VDSwitch {
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
public static void Left() {
keybd_event(0x11, 0, 0, UIntPtr.Zero); keybd_event(0x5B, 0, 0, UIntPtr.Zero);
keybd_event(0x25, 0, 0, UIntPtr.Zero); keybd_event(0x25, 0, 2, UIntPtr.Zero);
keybd_event(0x5B, 0, 2, UIntPtr.Zero); keybd_event(0x11, 0, 2, UIntPtr.Zero);
}
public static void Right() {
keybd_event(0x11, 0, 0, UIntPtr.Zero); keybd_event(0x5B, 0, 0, UIntPtr.Zero);
keybd_event(0x27, 0, 0, UIntPtr.Zero); keybd_event(0x27, 0, 2, UIntPtr.Zero);
keybd_event(0x5B, 0, 2, UIntPtr.Zero); keybd_event(0x11, 0, 2, UIntPtr.Zero);
}
}
'@
# Go far left first, then right to target index
for ($i = 0; $i -lt 20; $i++) { [VDSwitch]::Left(); Start-Sleep -Milliseconds 100 }
for ($i = 0; $i -lt ${index}; $i++) { [VDSwitch]::Right(); Start-Sleep -Milliseconds 100 }
"Switched to desktop ${index}"`;
const result = await runPowerShell(ps, { timeout: 15000 });
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
}
case 'remove': {
const ps = `
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class VDClose {
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
public static void CloseDesktop() {
keybd_event(0x11, 0, 0, UIntPtr.Zero); keybd_event(0x5B, 0, 0, UIntPtr.Zero);
keybd_event(0x73, 0, 0, UIntPtr.Zero); // F4
keybd_event(0x73, 0, 2, UIntPtr.Zero);
keybd_event(0x5B, 0, 2, UIntPtr.Zero); keybd_event(0x11, 0, 2, UIntPtr.Zero);
}
}
'@
[VDClose]::CloseDesktop()
"Current virtual desktop removed"`;
const result = await runPowerShell(ps, { timeout: 10000 });
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
}
}
},
);
}
+157
View File
@@ -0,0 +1,157 @@
/* 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,
};
},
);
}