@mokoconsulting/mcp-windows 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.gitattributes +94 -0
  2. package/.gitmessage +9 -0
  3. package/.mokogitea/ISSUE_TEMPLATE/adr.md +110 -0
  4. package/.mokogitea/ISSUE_TEMPLATE/bug_report.md +48 -0
  5. package/.mokogitea/ISSUE_TEMPLATE/config.yml +18 -0
  6. package/.mokogitea/ISSUE_TEMPLATE/documentation.md +52 -0
  7. package/.mokogitea/ISSUE_TEMPLATE/feature_request.md +51 -0
  8. package/.mokogitea/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
  9. package/.mokogitea/ISSUE_TEMPLATE/mcp_connection_issue.md +67 -0
  10. package/.mokogitea/ISSUE_TEMPLATE/mcp_tool_request.md +49 -0
  11. package/.mokogitea/ISSUE_TEMPLATE/question.md +82 -0
  12. package/.mokogitea/ISSUE_TEMPLATE/rfc.md +126 -0
  13. package/.mokogitea/ISSUE_TEMPLATE/security.md +51 -0
  14. package/.mokogitea/ISSUE_TEMPLATE/version.md +24 -0
  15. package/.mokogitea/branch-protection.yml +251 -0
  16. package/.mokogitea/workflows/auto-assign.yml +76 -0
  17. package/.mokogitea/workflows/auto-bump.yml +66 -0
  18. package/.mokogitea/workflows/auto-dev-issue.yml +207 -0
  19. package/.mokogitea/workflows/auto-release.yml +421 -0
  20. package/.mokogitea/workflows/branch-cleanup.yml +48 -0
  21. package/.mokogitea/workflows/cascade-dev.yml +10 -0
  22. package/.mokogitea/workflows/changelog-validation.yml +101 -0
  23. package/.mokogitea/workflows/ci-generic.yml +191 -0
  24. package/.mokogitea/workflows/cleanup.yml +87 -0
  25. package/.mokogitea/workflows/codeql-analysis.yml +115 -0
  26. package/.mokogitea/workflows/copilot-agent.yml +44 -0
  27. package/.mokogitea/workflows/deploy-manual.yml +126 -0
  28. package/.mokogitea/workflows/enterprise-firewall-setup.yml +758 -0
  29. package/.mokogitea/workflows/gitleaks.yml +92 -0
  30. package/.mokogitea/workflows/issue-branch.yml +73 -0
  31. package/.mokogitea/workflows/mcp-auto-release.yml +278 -0
  32. package/.mokogitea/workflows/mcp-build-test.yml +65 -0
  33. package/.mokogitea/workflows/mcp-sdk-check.yml +109 -0
  34. package/.mokogitea/workflows/mcp-tool-inventory.yml +61 -0
  35. package/.mokogitea/workflows/notify.yml +70 -0
  36. package/.mokogitea/workflows/npm-publish.yml +113 -0
  37. package/.mokogitea/workflows/pr-check.yml +534 -0
  38. package/.mokogitea/workflows/pre-release.yml +252 -0
  39. package/.mokogitea/workflows/rc-revert.yml +66 -0
  40. package/.mokogitea/workflows/repo-health.yml +712 -0
  41. package/.mokogitea/workflows/repository-cleanup.yml +525 -0
  42. package/.mokogitea/workflows/security-audit.yml +82 -0
  43. package/.mokogitea/workflows/standards-compliance.yml +2614 -0
  44. package/.mokogitea/workflows/sync-version-on-merge.yml +133 -0
  45. package/.mokogitea/workflows/update-server.yml +312 -0
  46. package/.mokogitea/workflows/workflow-sync-trigger.yml +73 -0
  47. package/CHANGELOG.md +130 -0
  48. package/CLAUDE.md +49 -0
  49. package/CONTRIBUTING.md +161 -0
  50. package/ISSUES.md +601 -0
  51. package/Makefile +70 -0
  52. package/README.md +80 -0
  53. package/automation/ci-issue-reporter.sh +237 -0
  54. package/config.example.json +18 -0
  55. package/dist/index.d.ts +3 -0
  56. package/dist/index.js +111 -0
  57. package/dist/shell.d.ts +50 -0
  58. package/dist/shell.js +209 -0
  59. package/dist/tools/apps.d.ts +3 -0
  60. package/dist/tools/apps.js +63 -0
  61. package/dist/tools/audio.d.ts +3 -0
  62. package/dist/tools/audio.js +142 -0
  63. package/dist/tools/audio_apps.d.ts +3 -0
  64. package/dist/tools/audio_apps.js +86 -0
  65. package/dist/tools/automation.d.ts +3 -0
  66. package/dist/tools/automation.js +261 -0
  67. package/dist/tools/bluetooth.d.ts +3 -0
  68. package/dist/tools/bluetooth.js +96 -0
  69. package/dist/tools/clipboard.d.ts +3 -0
  70. package/dist/tools/clipboard.js +118 -0
  71. package/dist/tools/config.d.ts +3 -0
  72. package/dist/tools/config.js +85 -0
  73. package/dist/tools/dialog.d.ts +3 -0
  74. package/dist/tools/dialog.js +72 -0
  75. package/dist/tools/display.d.ts +3 -0
  76. package/dist/tools/display.js +256 -0
  77. package/dist/tools/drives.d.ts +3 -0
  78. package/dist/tools/drives.js +98 -0
  79. package/dist/tools/environment.d.ts +3 -0
  80. package/dist/tools/environment.js +129 -0
  81. package/dist/tools/execute.d.ts +3 -0
  82. package/dist/tools/execute.js +28 -0
  83. package/dist/tools/filesystem.d.ts +3 -0
  84. package/dist/tools/filesystem.js +230 -0
  85. package/dist/tools/firewall.d.ts +3 -0
  86. package/dist/tools/firewall.js +108 -0
  87. package/dist/tools/hosts.d.ts +3 -0
  88. package/dist/tools/hosts.js +119 -0
  89. package/dist/tools/maintenance.d.ts +3 -0
  90. package/dist/tools/maintenance.js +236 -0
  91. package/dist/tools/netstat.d.ts +3 -0
  92. package/dist/tools/netstat.js +56 -0
  93. package/dist/tools/network.d.ts +3 -0
  94. package/dist/tools/network.js +70 -0
  95. package/dist/tools/notification.d.ts +3 -0
  96. package/dist/tools/notification.js +41 -0
  97. package/dist/tools/power.d.ts +3 -0
  98. package/dist/tools/power.js +104 -0
  99. package/dist/tools/printer.d.ts +3 -0
  100. package/dist/tools/printer.js +97 -0
  101. package/dist/tools/process.d.ts +3 -0
  102. package/dist/tools/process.js +54 -0
  103. package/dist/tools/process_kill.d.ts +3 -0
  104. package/dist/tools/process_kill.js +48 -0
  105. package/dist/tools/recycle_bin.d.ts +3 -0
  106. package/dist/tools/recycle_bin.js +108 -0
  107. package/dist/tools/registry.d.ts +3 -0
  108. package/dist/tools/registry.js +136 -0
  109. package/dist/tools/scheduler.d.ts +3 -0
  110. package/dist/tools/scheduler.js +116 -0
  111. package/dist/tools/service.d.ts +3 -0
  112. package/dist/tools/service.js +79 -0
  113. package/dist/tools/startup.d.ts +3 -0
  114. package/dist/tools/startup.js +159 -0
  115. package/dist/tools/storage.d.ts +3 -0
  116. package/dist/tools/storage.js +129 -0
  117. package/dist/tools/system.d.ts +3 -0
  118. package/dist/tools/system.js +84 -0
  119. package/dist/tools/system_mgmt.d.ts +3 -0
  120. package/dist/tools/system_mgmt.js +174 -0
  121. package/dist/tools/terminal.d.ts +3 -0
  122. package/dist/tools/terminal.js +80 -0
  123. package/dist/tools/theme.d.ts +3 -0
  124. package/dist/tools/theme.js +165 -0
  125. package/dist/tools/usb.d.ts +3 -0
  126. package/dist/tools/usb.js +52 -0
  127. package/dist/tools/virtual_desktop.d.ts +3 -0
  128. package/dist/tools/virtual_desktop.js +112 -0
  129. package/dist/tools/wifi.d.ts +3 -0
  130. package/dist/tools/wifi.js +136 -0
  131. package/dist/tools/window.d.ts +3 -0
  132. package/dist/tools/window.js +189 -0
  133. package/dist/tools/winget.d.ts +3 -0
  134. package/dist/tools/winget.js +79 -0
  135. package/dist/tools/wsl.d.ts +3 -0
  136. package/dist/tools/wsl.js +99 -0
  137. package/docs/API.md +63 -0
  138. package/docs/ARCHITECTURE.md +73 -0
  139. package/docs/INSTALLATION.md +102 -0
  140. package/docs/index.md +12 -0
  141. package/package.json +35 -0
  142. package/scripts/setup.mjs +123 -0
  143. package/src/index.ts +125 -0
  144. package/src/shell.ts +253 -0
  145. package/src/tools/apps.ts +76 -0
  146. package/src/tools/audio.ts +161 -0
  147. package/src/tools/audio_apps.ts +98 -0
  148. package/src/tools/automation.ts +297 -0
  149. package/src/tools/bluetooth.ts +114 -0
  150. package/src/tools/clipboard.ts +138 -0
  151. package/src/tools/config.ts +105 -0
  152. package/src/tools/dialog.ts +87 -0
  153. package/src/tools/display.ts +285 -0
  154. package/src/tools/drives.ts +124 -0
  155. package/src/tools/environment.ts +146 -0
  156. package/src/tools/execute.ts +35 -0
  157. package/src/tools/filesystem.ts +273 -0
  158. package/src/tools/firewall.ts +125 -0
  159. package/src/tools/hosts.ts +135 -0
  160. package/src/tools/maintenance.ts +299 -0
  161. package/src/tools/netstat.ts +72 -0
  162. package/src/tools/network.ts +84 -0
  163. package/src/tools/notification.ts +50 -0
  164. package/src/tools/power.ts +123 -0
  165. package/src/tools/printer.ts +114 -0
  166. package/src/tools/process.ts +80 -0
  167. package/src/tools/process_kill.ts +57 -0
  168. package/src/tools/recycle_bin.ts +126 -0
  169. package/src/tools/registry.ts +165 -0
  170. package/src/tools/scheduler.ts +140 -0
  171. package/src/tools/service.ts +102 -0
  172. package/src/tools/startup.ts +180 -0
  173. package/src/tools/storage.ts +141 -0
  174. package/src/tools/system.ts +99 -0
  175. package/src/tools/system_mgmt.ts +190 -0
  176. package/src/tools/terminal.ts +117 -0
  177. package/src/tools/theme.ts +205 -0
  178. package/src/tools/usb.ts +65 -0
  179. package/src/tools/virtual_desktop.ts +122 -0
  180. package/src/tools/wifi.ts +157 -0
  181. package/src/tools/window.ts +211 -0
  182. package/src/tools/winget.ts +100 -0
  183. package/src/tools/wsl.ts +112 -0
  184. package/tsconfig.json +19 -0
@@ -0,0 +1,84 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_network_info (#22)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { runPowerShell } from '../shell.js';
9
+
10
+ export function registerNetworkTools(server: McpServer): void {
11
+ server.tool(
12
+ 'windows_network_info',
13
+ 'Get network configuration: adapters, IPs, DNS, gateway, Wi-Fi status, and connectivity.',
14
+ {},
15
+ async () => {
16
+ const ps = `
17
+ $adapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | ForEach-Object {
18
+ $ipInfo = Get-NetIPAddress -InterfaceIndex $_.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue
19
+ $dns = (Get-DnsClientServerAddress -InterfaceIndex $_.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue).ServerAddresses
20
+ $gw = (Get-NetRoute -InterfaceIndex $_.ifIndex -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue).NextHop
21
+ [PSCustomObject]@{
22
+ Name = $_.Name
23
+ Description = $_.InterfaceDescription
24
+ Type = $_.MediaType
25
+ Status = $_.Status
26
+ Speed = $_.LinkSpeed
27
+ MAC = $_.MacAddress
28
+ IP = if ($ipInfo) { $ipInfo.IPAddress -join ', ' } else { 'N/A' }
29
+ Subnet = if ($ipInfo) { $ipInfo.PrefixLength -join ', ' } else { 'N/A' }
30
+ Gateway = if ($gw) { $gw -join ', ' } else { 'N/A' }
31
+ DNS = if ($dns) { $dns -join ', ' } else { 'N/A' }
32
+ }
33
+ }
34
+
35
+ # Wi-Fi info
36
+ $wifi = $null
37
+ try {
38
+ $wifiProfile = netsh wlan show interfaces 2>$null
39
+ if ($wifiProfile) {
40
+ $ssid = ($wifiProfile | Select-String 'SSID\s+:' | Select-Object -First 1) -replace '.*:\s*',''
41
+ $signal = ($wifiProfile | Select-String 'Signal' | Select-Object -First 1) -replace '.*:\s*',''
42
+ $auth = ($wifiProfile | Select-String 'Authentication' | Select-Object -First 1) -replace '.*:\s*',''
43
+ $wifi = [PSCustomObject]@{ SSID = $ssid.Trim(); Signal = $signal.Trim(); Security = $auth.Trim() }
44
+ }
45
+ } catch {}
46
+
47
+ # Connectivity
48
+ $connected = Test-Connection -ComputerName 8.8.8.8 -Count 1 -Quiet -ErrorAction SilentlyContinue
49
+
50
+ [PSCustomObject]@{
51
+ Adapters = $adapters
52
+ WiFi = $wifi
53
+ InternetConnected = $connected
54
+ } | ConvertTo-Json -Depth 4 -Compress`;
55
+
56
+ const result = await runPowerShell(ps, { timeout: 15000 });
57
+ if (result.exitCode !== 0) {
58
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
59
+ }
60
+
61
+ const info = JSON.parse(result.stdout);
62
+ const lines: string[] = [];
63
+
64
+ lines.push(`Internet: ${info.InternetConnected ? 'Connected' : 'Disconnected'}`);
65
+
66
+ if (info.WiFi) {
67
+ lines.push(`Wi-Fi: ${info.WiFi.SSID} (${info.WiFi.Signal}, ${info.WiFi.Security})`);
68
+ }
69
+
70
+ lines.push('');
71
+ lines.push('Adapters:');
72
+
73
+ const adapters = Array.isArray(info.Adapters) ? info.Adapters : [info.Adapters];
74
+ for (const a of adapters.filter(Boolean)) {
75
+ lines.push(` ${a.Name} (${a.Description})`);
76
+ lines.push(` IP: ${a.IP}/${a.Subnet} Gateway: ${a.Gateway}`);
77
+ lines.push(` DNS: ${a.DNS}`);
78
+ lines.push(` MAC: ${a.MAC} Speed: ${a.Speed}`);
79
+ }
80
+
81
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
82
+ },
83
+ );
84
+ }
@@ -0,0 +1,50 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_notification_send (#20)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ export function registerNotificationTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_notification_send',
14
+ 'Send a Windows toast notification with title, body, and optional icon.',
15
+ {
16
+ title: z.string().describe('Notification title'),
17
+ body: z.string().describe('Notification body text'),
18
+ icon: z.string().optional().describe('Path to icon file (optional)'),
19
+ sound: z.boolean().default(true).describe('Play notification sound'),
20
+ },
21
+ async ({ title, body, icon, sound }) => {
22
+ const iconParam = icon
23
+ ? `$notify.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon('${icon.replace(/'/g, "''")}');`
24
+ : `$notify.Icon = [System.Drawing.SystemIcons]::Information;`;
25
+
26
+ const ps = `
27
+ Add-Type -AssemblyName System.Windows.Forms
28
+ Add-Type -AssemblyName System.Drawing
29
+
30
+ $notify = New-Object System.Windows.Forms.NotifyIcon
31
+ ${iconParam}
32
+ $notify.Visible = $true
33
+ $notify.BalloonTipTitle = '${title.replace(/'/g, "''")}'
34
+ $notify.BalloonTipText = '${body.replace(/'/g, "''")}'
35
+ $notify.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Info
36
+ $notify.ShowBalloonTip(5000)
37
+
38
+ # Keep alive briefly so notification displays
39
+ Start-Sleep -Milliseconds 100
40
+ $notify.Dispose()
41
+ "Notification sent: ${title.replace(/"/g, '\\"')}"`;
42
+
43
+ const result = await runPowerShell(ps, { timeout: 10000 });
44
+ return {
45
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
46
+ isError: result.exitCode !== 0,
47
+ };
48
+ },
49
+ );
50
+ }
@@ -0,0 +1,123 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_power_get (#12), windows_power_action (#13)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ export function registerPowerTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_power_get',
14
+ 'Get power state: battery level, AC/battery, power plan, screen/sleep timeouts.',
15
+ {},
16
+ async () => {
17
+ const ps = `
18
+ $battery = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue
19
+ $plan = powercfg /getactivescheme 2>$null
20
+ $planName = if ($plan) { ($plan -replace '^.*\\((.*)\\).*$','$1').Trim() } else { 'Unknown' }
21
+
22
+ # Get timeout values
23
+ $acScreen = (powercfg /query SCHEME_CURRENT SUB_VIDEO VIDEOIDLE 2>$null | Select-String 'Current AC Power Setting Index' | ForEach-Object { ($_ -split '0x')[1] }) -as [int]
24
+ $dcScreen = (powercfg /query SCHEME_CURRENT SUB_VIDEO VIDEOIDLE 2>$null | Select-String 'Current DC Power Setting Index' | ForEach-Object { ($_ -split '0x')[1] }) -as [int]
25
+ $acSleep = (powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE 2>$null | Select-String 'Current AC Power Setting Index' | ForEach-Object { ($_ -split '0x')[1] }) -as [int]
26
+ $dcSleep = (powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE 2>$null | Select-String 'Current DC Power Setting Index' | ForEach-Object { ($_ -split '0x')[1] }) -as [int]
27
+
28
+ [PSCustomObject]@{
29
+ HasBattery = $null -ne $battery
30
+ BatteryPct = if ($battery) { $battery.EstimatedChargeRemaining } else { $null }
31
+ Charging = if ($battery) { $battery.BatteryStatus -eq 2 } else { $null }
32
+ ACPower = if ($battery) { $battery.BatteryStatus -eq 2 -or $battery.BatteryStatus -eq 6 } else { $true }
33
+ TimeRemaining = if ($battery -and $battery.EstimatedRunTime -and $battery.EstimatedRunTime -lt 71582788) { "$([math]::Floor($battery.EstimatedRunTime / 60))h $($battery.EstimatedRunTime % 60)m" } else { $null }
34
+ PowerPlan = $planName
35
+ ScreenTimeout_AC = if ($acScreen) { "$([math]::Floor($acScreen / 60))m" } else { 'Never' }
36
+ ScreenTimeout_DC = if ($dcScreen) { "$([math]::Floor($dcScreen / 60))m" } else { 'Never' }
37
+ SleepTimeout_AC = if ($acSleep) { "$([math]::Floor($acSleep / 60))m" } else { 'Never' }
38
+ SleepTimeout_DC = if ($dcSleep) { "$([math]::Floor($dcSleep / 60))m" } else { 'Never' }
39
+ } | ConvertTo-Json -Compress`;
40
+
41
+ const result = await runPowerShell(ps, { timeout: 30000 });
42
+ if (result.exitCode !== 0) {
43
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
44
+ }
45
+
46
+ const info = JSON.parse(result.stdout);
47
+ const lines: string[] = [];
48
+
49
+ if (info.HasBattery) {
50
+ const icon = info.Charging ? '🔌' : '🔋';
51
+ lines.push(`${icon} Battery: ${info.BatteryPct}%${info.Charging ? ' (charging)' : ''}`);
52
+ if (info.TimeRemaining) lines.push(`Estimated remaining: ${info.TimeRemaining}`);
53
+ } else {
54
+ lines.push(`AC Power (no battery)`);
55
+ }
56
+
57
+ lines.push(`Power Plan: ${info.PowerPlan}`);
58
+ lines.push(``);
59
+ lines.push(`Screen timeout: ${info.ScreenTimeout_AC} (AC) / ${info.ScreenTimeout_DC} (battery)`);
60
+ lines.push(`Sleep timeout: ${info.SleepTimeout_AC} (AC) / ${info.SleepTimeout_DC} (battery)`);
61
+
62
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
63
+ },
64
+ );
65
+
66
+ server.tool(
67
+ 'windows_power_action',
68
+ 'Execute power actions: sleep, hibernate, lock, shutdown, restart, or switch power plan.',
69
+ {
70
+ action: z.enum(['sleep', 'hibernate', 'lock', 'shutdown', 'restart', 'logoff', 'plan']).describe('Power action'),
71
+ delay: z.number().optional().describe('Delay in seconds (for shutdown/restart)'),
72
+ cancel: z.boolean().optional().describe('Cancel a scheduled shutdown/restart'),
73
+ plan: z.enum(['balanced', 'performance', 'powersaver']).optional().describe('Power plan to switch to (when action=plan)'),
74
+ },
75
+ async ({ action, delay, cancel, plan }) => {
76
+ if (cancel) {
77
+ const result = await runPowerShell('shutdown /a 2>&1; "Scheduled shutdown cancelled"');
78
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
79
+ }
80
+
81
+ let ps: string;
82
+ const delayArg = delay ? `/t ${delay}` : '';
83
+
84
+ switch (action) {
85
+ case 'sleep':
86
+ ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState('Suspend', $false, $false); "Sleeping..."`;
87
+ break;
88
+ case 'hibernate':
89
+ ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState('Hibernate', $false, $false); "Hibernating..."`;
90
+ break;
91
+ case 'lock':
92
+ ps = `rundll32.exe user32.dll,LockWorkStation; "Workstation locked"`;
93
+ break;
94
+ case 'shutdown':
95
+ ps = `shutdown /s /f ${delayArg}; "Shutdown initiated${delay ? ` in ${delay}s` : ''}"`;
96
+ break;
97
+ case 'restart':
98
+ ps = `shutdown /r /f ${delayArg}; "Restart initiated${delay ? ` in ${delay}s` : ''}"`;
99
+ break;
100
+ case 'logoff':
101
+ ps = `shutdown /l; "Logging off..."`;
102
+ break;
103
+ case 'plan':
104
+ if (!plan) {
105
+ return { content: [{ type: 'text', text: 'Specify a plan: balanced, performance, or powersaver' }], isError: true };
106
+ }
107
+ const planGuids: Record<string, string> = {
108
+ balanced: '381b4222-f694-41f0-9685-ff5bb260df2e',
109
+ performance: '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c',
110
+ powersaver: 'a1841308-3541-4fab-bc81-f71556f20b4a',
111
+ };
112
+ ps = `powercfg /setactive ${planGuids[plan]}; $p = powercfg /getactivescheme; "Switched to: $($p -replace '^.*\\((.*)\\).*$','$1')"`;
113
+ break;
114
+ }
115
+
116
+ const result = await runPowerShell(ps, { timeout: 30000 });
117
+ return {
118
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
119
+ isError: result.exitCode !== 0,
120
+ };
121
+ },
122
+ );
123
+ }
@@ -0,0 +1,114 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_printer_list (#50)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ export function registerPrinterTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_printer_list',
14
+ 'List printers, show print queues, set default, or clear a queue.',
15
+ {
16
+ action: z.enum(['list', 'queue', 'set_default', 'clear_queue']).default('list').describe('Action'),
17
+ printer: z.string().optional().describe('Printer name (for queue/set_default/clear_queue)'),
18
+ },
19
+ async ({ action, printer }) => {
20
+ switch (action) {
21
+ case 'list': {
22
+ const ps = `
23
+ Get-Printer -ErrorAction SilentlyContinue | ForEach-Object {
24
+ $default = if ((Get-CimInstance Win32_Printer -Filter "Name='$($_.Name.Replace("'","''"))'" -ErrorAction SilentlyContinue).Default) { $true } else { $false }
25
+ [PSCustomObject]@{
26
+ Name = $_.Name
27
+ Status = $_.PrinterStatus
28
+ Type = $_.Type
29
+ Port = $_.PortName
30
+ Driver = $_.DriverName
31
+ Default = $default
32
+ Shared = $_.Shared
33
+ }
34
+ } | ConvertTo-Json -Depth 3 -Compress`;
35
+
36
+ const result = await runPowerShell(ps, { timeout: 45000 });
37
+ if (result.exitCode !== 0) {
38
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
39
+ }
40
+ if (!result.stdout) {
41
+ return { content: [{ type: 'text', text: 'No printers found.' }] };
42
+ }
43
+
44
+ const printers = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
45
+ const lines = printers.map((p: { Name: string; Status: string; Driver: string; Port: string; Default: boolean }) => {
46
+ const def = p.Default ? ' *' : ' ';
47
+ return `${def} ${(p.Name || '').padEnd(35).slice(0, 35)} ${(p.Driver || '').padEnd(25).slice(0, 25)} ${p.Port || ''}`;
48
+ });
49
+
50
+ const header = ` ${'Name'.padEnd(35)} ${'Driver'.padEnd(25)} Port`;
51
+ return {
52
+ content: [{ type: 'text', text: `${header}\n${'─'.repeat(80)}\n${lines.join('\n')}\n\n${printers.length} printers (* = default)` }],
53
+ };
54
+ }
55
+
56
+ case 'queue': {
57
+ if (!printer) {
58
+ return { content: [{ type: 'text', text: 'Queue requires printer name.' }], isError: true };
59
+ }
60
+ const ps = `
61
+ Get-PrintJob -PrinterName '${printer.replace(/'/g, "''")}' -ErrorAction Stop | ForEach-Object {
62
+ [PSCustomObject]@{
63
+ Id = $_.Id
64
+ Document = $_.DocumentName
65
+ Status = $_.JobStatus
66
+ Pages = $_.TotalPages
67
+ Size = "$([math]::Round($_.Size / 1KB, 1)) KB"
68
+ Submitted = $_.SubmittedTime.ToString('yyyy-MM-dd HH:mm')
69
+ }
70
+ } | ConvertTo-Json -Depth 3 -Compress`;
71
+
72
+ const result = await runPowerShell(ps, { timeout: 10000 });
73
+ if (result.exitCode !== 0) {
74
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
75
+ }
76
+ if (!result.stdout) {
77
+ return { content: [{ type: 'text', text: `Print queue for "${printer}" is empty.` }] };
78
+ }
79
+
80
+ const jobs = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
81
+ const lines = jobs.map((j: { Id: number; Document: string; Status: string; Pages: number; Size: string }) =>
82
+ ` #${j.Id} ${(j.Document || '').padEnd(30).slice(0, 30)} ${(j.Status || '').padEnd(12)} ${j.Pages} pg ${j.Size}`,
83
+ );
84
+ return { content: [{ type: 'text', text: `Queue for "${printer}":\n${lines.join('\n')}` }] };
85
+ }
86
+
87
+ case 'set_default': {
88
+ if (!printer) {
89
+ return { content: [{ type: 'text', text: 'set_default requires printer name.' }], isError: true };
90
+ }
91
+ const ps = `
92
+ $p = Get-CimInstance Win32_Printer -Filter "Name='${printer.replace(/'/g, "''")}'" -ErrorAction Stop
93
+ Invoke-CimMethod -InputObject $p -MethodName SetDefaultPrinter | Out-Null
94
+ "Default printer set to: ${printer}"`;
95
+ const result = await runPowerShell(ps, { timeout: 10000 });
96
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
97
+ }
98
+
99
+ case 'clear_queue': {
100
+ if (!printer) {
101
+ return { content: [{ type: 'text', text: 'clear_queue requires printer name.' }], isError: true };
102
+ }
103
+ const ps = `
104
+ $jobs = Get-PrintJob -PrinterName '${printer.replace(/'/g, "''")}' -ErrorAction Stop
105
+ $count = @($jobs).Count
106
+ $jobs | Remove-PrintJob -ErrorAction SilentlyContinue
107
+ "Cleared $count job(s) from ${printer}"`;
108
+ const result = await runPowerShell(ps, { timeout: 10000 });
109
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
110
+ }
111
+ }
112
+ },
113
+ );
114
+ }
@@ -0,0 +1,80 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_process_list — List running processes (#2)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ interface ProcessInfo {
12
+ PID: number;
13
+ Name: string;
14
+ CPU: number;
15
+ MemoryMB: number;
16
+ WindowTitle: string;
17
+ Path: string;
18
+ }
19
+
20
+ export function registerProcessTools(server: McpServer): void {
21
+ server.tool(
22
+ 'windows_process_list',
23
+ 'List running processes with PID, name, CPU, memory, window title, and path.',
24
+ {
25
+ filter: z.string().optional().describe('Filter by process name (substring match)'),
26
+ sort: z.enum(['cpu', 'memory', 'name']).default('memory').describe('Sort order'),
27
+ limit: z.number().default(50).describe('Max results to return'),
28
+ },
29
+ async ({ filter, sort, limit }) => {
30
+ const filterClause = filter
31
+ ? `| Where-Object { $_.Name -like '*${filter.replace(/'/g, "''")}*' }`
32
+ : '';
33
+
34
+ const sortClause = sort === 'cpu'
35
+ ? '| Sort-Object CPU -Descending'
36
+ : sort === 'name'
37
+ ? '| Sort-Object Name'
38
+ : '| Sort-Object WorkingSet64 -Descending';
39
+
40
+ const ps = `
41
+ Get-Process ${filterClause} ${sortClause} | Select-Object -First ${limit} |
42
+ ForEach-Object {
43
+ [PSCustomObject]@{
44
+ PID = $_.Id
45
+ Name = $_.ProcessName
46
+ CPU = [math]::Round($_.CPU, 1)
47
+ MemoryMB = [math]::Round($_.WorkingSet64 / 1MB, 1)
48
+ WindowTitle = $_.MainWindowTitle
49
+ Path = $_.Path
50
+ }
51
+ } | ConvertTo-Json -Depth 3 -Compress`;
52
+
53
+ const result = await runPowerShell(ps);
54
+
55
+ if (result.exitCode !== 0) {
56
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
57
+ }
58
+
59
+ let processes: ProcessInfo[] = [];
60
+ if (result.stdout) {
61
+ const parsed = JSON.parse(result.stdout);
62
+ processes = Array.isArray(parsed) ? parsed : [parsed];
63
+ }
64
+
65
+ const lines = processes.map(p =>
66
+ `${String(p.PID).padStart(7)} ${p.Name.padEnd(25).slice(0, 25)} ${String(p.CPU ?? 0).padStart(8)}s ${String(p.MemoryMB).padStart(8)} MB ${p.WindowTitle || ''}`,
67
+ );
68
+
69
+ const header = `${'PID'.padStart(7)} ${'Name'.padEnd(25)} ${'CPU'.padStart(8)} ${'Memory'.padStart(8)} Window Title`;
70
+ const separator = '-'.repeat(90);
71
+
72
+ return {
73
+ content: [{
74
+ type: 'text',
75
+ text: `${header}\n${separator}\n${lines.join('\n')}\n\n${processes.length} processes`,
76
+ }],
77
+ };
78
+ },
79
+ );
80
+ }
@@ -0,0 +1,57 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_process_kill (#3)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ export function registerProcessKillTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_process_kill',
14
+ 'Terminate running processes by PID or name.',
15
+ {
16
+ pid: z.number().optional().describe('Process ID to kill'),
17
+ name: z.string().optional().describe('Process name to kill (all matching)'),
18
+ force: z.boolean().default(false).describe('Force termination'),
19
+ },
20
+ async ({ pid, name, force }) => {
21
+ if (!pid && !name) {
22
+ return { content: [{ type: 'text', text: 'Provide either pid or name.' }], isError: true };
23
+ }
24
+
25
+ const forceFlag = force ? ' -Force' : '';
26
+ let ps: string;
27
+
28
+ if (pid) {
29
+ ps = `
30
+ $p = Get-Process -Id ${pid} -ErrorAction SilentlyContinue
31
+ if ($p) {
32
+ $n = $p.ProcessName
33
+ Stop-Process -Id ${pid}${forceFlag} -ErrorAction Stop
34
+ "Killed PID ${pid} ($n)"
35
+ } else {
36
+ "No process with PID ${pid}"
37
+ }`;
38
+ } else {
39
+ ps = `
40
+ $procs = Get-Process -Name '${name!.replace(/'/g, "''")}' -ErrorAction SilentlyContinue
41
+ if ($procs) {
42
+ $count = @($procs).Count
43
+ $procs | Stop-Process${forceFlag} -ErrorAction Stop
44
+ "Killed $count process(es) named '${name}'"
45
+ } else {
46
+ "No processes named '${name}'"
47
+ }`;
48
+ }
49
+
50
+ const result = await runPowerShell(ps);
51
+ return {
52
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
53
+ isError: result.exitCode !== 0,
54
+ };
55
+ },
56
+ );
57
+ }
@@ -0,0 +1,126 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_recycle_bin (#26)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ export function registerRecycleBinTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_recycle_bin',
14
+ 'Manage the Recycle Bin: list items, get size, restore items, or empty.',
15
+ {
16
+ action: z.enum(['list', 'size', 'empty', 'restore']).default('list').describe('Action'),
17
+ filter: z.string().optional().describe('Filter by filename (for list/restore)'),
18
+ limit: z.number().default(30).describe('Max items (for list)'),
19
+ },
20
+ async ({ action, filter, limit }) => {
21
+ switch (action) {
22
+ case 'size': {
23
+ const ps = `
24
+ $shell = New-Object -ComObject Shell.Application
25
+ $bin = $shell.Namespace(10)
26
+ $items = $bin.Items()
27
+ $count = $items.Count
28
+ $totalSize = 0
29
+ for ($i = 0; $i -lt $count; $i++) {
30
+ $totalSize += $bin.GetDetailsOf($items.Item($i), 2) -replace '[^0-9]','' -as [long]
31
+ }
32
+ [PSCustomObject]@{
33
+ Count = $count
34
+ SizeMB = [math]::Round($totalSize / 1MB, 1)
35
+ } | ConvertTo-Json -Compress`;
36
+
37
+ const result = await runPowerShell(ps, { timeout: 15000 });
38
+ if (result.exitCode !== 0) {
39
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
40
+ }
41
+ const info = JSON.parse(result.stdout);
42
+ return { content: [{ type: 'text', text: `Recycle Bin: ${info.Count} items, ${info.SizeMB} MB` }] };
43
+ }
44
+
45
+ case 'list': {
46
+ const filterClause = filter
47
+ ? `| Where-Object { $_.Name -like '*${filter.replace(/'/g, "''")}*' }`
48
+ : '';
49
+
50
+ const ps = `
51
+ $shell = New-Object -ComObject Shell.Application
52
+ $bin = $shell.Namespace(10)
53
+ $items = @()
54
+ foreach ($item in $bin.Items()) {
55
+ $items += [PSCustomObject]@{
56
+ Name = $item.Name
57
+ OriginalPath = $bin.GetDetailsOf($item, 1)
58
+ Size = $bin.GetDetailsOf($item, 2)
59
+ DeletedDate = $bin.GetDetailsOf($item, 3)
60
+ Type = $bin.GetDetailsOf($item, 4)
61
+ }
62
+ }
63
+ $items ${filterClause} | Select-Object -First ${limit} | ConvertTo-Json -Depth 3 -Compress`;
64
+
65
+ const result = await runPowerShell(ps, { timeout: 20000 });
66
+ if (result.exitCode !== 0) {
67
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
68
+ }
69
+ if (!result.stdout) {
70
+ return { content: [{ type: 'text', text: 'Recycle Bin is empty.' }] };
71
+ }
72
+
73
+ const items = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
74
+ const lines = items.map((i: { Name: string; OriginalPath: string; Size: string; DeletedDate: string }) =>
75
+ `${(i.Name || '').padEnd(35).slice(0, 35)} ${(i.Size || '').padStart(10)} ${(i.DeletedDate || '').padEnd(20)} ${(i.OriginalPath || '').slice(0, 40)}`,
76
+ );
77
+
78
+ const header = `${'Name'.padEnd(35)} ${'Size'.padStart(10)} ${'Deleted'.padEnd(20)} Original Path`;
79
+ return {
80
+ content: [{ type: 'text', text: `${header}\n${'─'.repeat(110)}\n${lines.join('\n')}\n\n${items.length} items` }],
81
+ };
82
+ }
83
+
84
+ case 'empty': {
85
+ const ps = `
86
+ $shell = New-Object -ComObject Shell.Application
87
+ $count = $shell.Namespace(10).Items().Count
88
+ if ($count -eq 0) { "Recycle Bin is already empty." }
89
+ else {
90
+ Clear-RecycleBin -Force -ErrorAction Stop
91
+ "Emptied Recycle Bin ($count items removed)"
92
+ }`;
93
+ const result = await runPowerShell(ps, { timeout: 15000 });
94
+ return {
95
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
96
+ isError: result.exitCode !== 0,
97
+ };
98
+ }
99
+
100
+ case 'restore': {
101
+ if (!filter) {
102
+ return { content: [{ type: 'text', text: 'Restore requires a filter to identify which item(s) to restore.' }], isError: true };
103
+ }
104
+ const ps = `
105
+ $shell = New-Object -ComObject Shell.Application
106
+ $bin = $shell.Namespace(10)
107
+ $restored = 0
108
+ foreach ($item in $bin.Items()) {
109
+ if ($item.Name -like '*${filter.replace(/'/g, "''")}*') {
110
+ $origPath = $bin.GetDetailsOf($item, 1)
111
+ $item.InvokeVerb('undelete')
112
+ $restored++
113
+ }
114
+ }
115
+ if ($restored -gt 0) { "Restored $restored item(s)" } else { "No matching items found" }`;
116
+
117
+ const result = await runPowerShell(ps, { timeout: 15000 });
118
+ return {
119
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
120
+ isError: result.exitCode !== 0,
121
+ };
122
+ }
123
+ }
124
+ },
125
+ );
126
+ }