@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,205 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_theme_get (#52), windows_theme_set (#53),
5
+ * windows_focus_mode (#55), windows_default_apps (#56)
6
+ */
7
+
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { z } from 'zod';
10
+ import { runPowerShell } from '../shell.js';
11
+
12
+ export function registerThemeTools(server: McpServer): void {
13
+ server.tool(
14
+ 'windows_theme_get',
15
+ 'Get current Windows theme: dark/light mode, accent color, wallpaper, transparency, taskbar alignment.',
16
+ {},
17
+ async () => {
18
+ const ps = `
19
+ $personalize = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize'
20
+ $accent = 'HKCU:\\Software\\Microsoft\\Windows\\DWM'
21
+ $taskbar = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced'
22
+ $wallpaper = (Get-ItemProperty -Path 'HKCU:\\Control Panel\\Desktop' -Name Wallpaper -ErrorAction SilentlyContinue).Wallpaper
23
+
24
+ $appsDark = (Get-ItemProperty -Path $personalize -Name AppsUseLightTheme -ErrorAction SilentlyContinue).AppsUseLightTheme -eq 0
25
+ $systemDark = (Get-ItemProperty -Path $personalize -Name SystemUsesLightTheme -ErrorAction SilentlyContinue).SystemUsesLightTheme -eq 0
26
+ $transparency = (Get-ItemProperty -Path $personalize -Name EnableTransparency -ErrorAction SilentlyContinue).EnableTransparency -eq 1
27
+ $accentColor = (Get-ItemProperty -Path $accent -Name AccentColor -ErrorAction SilentlyContinue).AccentColor
28
+ $taskbarAlign = (Get-ItemProperty -Path $taskbar -Name TaskbarAl -ErrorAction SilentlyContinue).TaskbarAl
29
+
30
+ $accentHex = if ($accentColor) {
31
+ $b = ($accentColor -band 0xFF0000) -shr 16
32
+ $g = ($accentColor -band 0x00FF00) -shr 8
33
+ $r = ($accentColor -band 0x0000FF)
34
+ '#{0:X2}{1:X2}{2:X2}' -f $r, $g, $b
35
+ } else { 'Unknown' }
36
+
37
+ [PSCustomObject]@{
38
+ AppsDarkMode = $appsDark
39
+ SystemDarkMode = $systemDark
40
+ AccentColor = $accentHex
41
+ Transparency = $transparency
42
+ Wallpaper = $wallpaper
43
+ TaskbarAlignment = if ($taskbarAlign -eq 0) { 'Left' } else { 'Center' }
44
+ } | ConvertTo-Json -Compress`;
45
+
46
+ const result = await runPowerShell(ps, { timeout: 10000 });
47
+ if (result.exitCode !== 0) {
48
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
49
+ }
50
+
51
+ const t = JSON.parse(result.stdout);
52
+ return {
53
+ content: [{
54
+ type: 'text',
55
+ text: [
56
+ `Dark mode: Apps=${t.AppsDarkMode}, System=${t.SystemDarkMode}`,
57
+ `Accent color: ${t.AccentColor}`,
58
+ `Transparency: ${t.Transparency ? 'On' : 'Off'}`,
59
+ `Taskbar: ${t.TaskbarAlignment}`,
60
+ `Wallpaper: ${t.Wallpaper || '(none)'}`,
61
+ ].join('\n'),
62
+ }],
63
+ };
64
+ },
65
+ );
66
+
67
+ server.tool(
68
+ 'windows_theme_set',
69
+ 'Set dark/light mode, accent color, wallpaper, transparency, or taskbar alignment.',
70
+ {
71
+ dark_mode: z.enum(['on', 'off']).optional().describe('Set dark mode for apps and system'),
72
+ wallpaper: z.string().optional().describe('Wallpaper file path'),
73
+ wallpaper_fit: z.enum(['fill', 'fit', 'stretch', 'tile', 'center', 'span']).optional().describe('Wallpaper fit mode'),
74
+ transparency: z.enum(['on', 'off']).optional().describe('Transparency effects'),
75
+ taskbar_align: z.enum(['left', 'center']).optional().describe('Taskbar alignment'),
76
+ },
77
+ async ({ dark_mode, wallpaper, wallpaper_fit, transparency, taskbar_align }) => {
78
+ const commands: string[] = [];
79
+
80
+ if (dark_mode) {
81
+ const val = dark_mode === 'on' ? 0 : 1;
82
+ commands.push(`Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize' -Name AppsUseLightTheme -Value ${val}`);
83
+ commands.push(`Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize' -Name SystemUsesLightTheme -Value ${val}`);
84
+ commands.push(`"Dark mode: ${dark_mode}"`);
85
+ }
86
+
87
+ if (transparency) {
88
+ const val = transparency === 'on' ? 1 : 0;
89
+ commands.push(`Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize' -Name EnableTransparency -Value ${val}`);
90
+ commands.push(`"Transparency: ${transparency}"`);
91
+ }
92
+
93
+ if (taskbar_align) {
94
+ const val = taskbar_align === 'left' ? 0 : 1;
95
+ commands.push(`Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced' -Name TaskbarAl -Value ${val}`);
96
+ commands.push(`"Taskbar: ${taskbar_align}"`);
97
+ }
98
+
99
+ if (wallpaper) {
100
+ const fitMap: Record<string, string> = { fill: '10', fit: '6', stretch: '2', tile: '0', center: '0', span: '22' };
101
+ const fit = wallpaper_fit || 'fill';
102
+ commands.push(`
103
+ Set-ItemProperty -Path 'HKCU:\\Control Panel\\Desktop' -Name WallpaperStyle -Value '${fitMap[fit]}'
104
+ Set-ItemProperty -Path 'HKCU:\\Control Panel\\Desktop' -Name TileWallpaper -Value '${fit === 'tile' ? '1' : '0'}'
105
+ 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); }'
106
+ [Wallpaper]::SystemParametersInfo(0x0014, 0, '${wallpaper.replace(/'/g, "''")}', 0x01 -bor 0x02) | Out-Null
107
+ "Wallpaper set: ${wallpaper} (${fit})"
108
+ `);
109
+ }
110
+
111
+ if (commands.length === 0) {
112
+ return { content: [{ type: 'text', text: 'No changes specified.' }], isError: true };
113
+ }
114
+
115
+ const result = await runPowerShell(commands.join('\n'), { timeout: 15000 });
116
+ return {
117
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
118
+ isError: result.exitCode !== 0,
119
+ };
120
+ },
121
+ );
122
+
123
+ server.tool(
124
+ 'windows_focus_mode',
125
+ 'Get or set Windows Focus Assist / Do Not Disturb mode.',
126
+ {
127
+ action: z.enum(['get', 'priority', 'alarms', 'off']).default('get').describe('Get status or set mode'),
128
+ },
129
+ async ({ action }) => {
130
+ if (action === 'get') {
131
+ const ps = `
132
+ $regPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.notifications.quiethourssettings\\windows.data.notifications.quiethourssettings'
133
+ $mode = 'Unknown'
134
+ try {
135
+ $val = (Get-ItemProperty -Path $regPath -ErrorAction Stop).Data
136
+ if ($val) {
137
+ # The focus assist state is encoded in the binary blob
138
+ $mode = 'Check via Settings app (binary registry format)'
139
+ }
140
+ } catch { $mode = 'Off (or unable to read)' }
141
+ "Focus Assist: $mode"`;
142
+ const result = await runPowerShell(ps, { timeout: 10000 });
143
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
144
+ }
145
+
146
+ // Setting focus assist requires ms-settings URI
147
+ const ps = `Start-Process 'ms-settings:quiethours'; "Opened Focus Assist settings. Mode requested: ${action}"`;
148
+ const result = await runPowerShell(ps, { timeout: 10000 });
149
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
150
+ },
151
+ );
152
+
153
+ server.tool(
154
+ 'windows_default_apps',
155
+ 'Get default applications or open the default apps settings page.',
156
+ {
157
+ action: z.enum(['get', 'open_settings']).default('get').describe('Get defaults or open settings'),
158
+ extension: z.string().optional().describe('File extension to check (e.g. ".pdf")'),
159
+ },
160
+ async ({ action, extension }) => {
161
+ if (action === 'open_settings') {
162
+ await runPowerShell('Start-Process "ms-settings:defaultapps"');
163
+ return { content: [{ type: 'text', text: 'Opened Default Apps settings.' }] };
164
+ }
165
+
166
+ if (extension) {
167
+ const ps = `
168
+ $assoc = cmd /c assoc ${extension} 2>$null
169
+ $ftype = if ($assoc) { $type = ($assoc -split '=')[1]; cmd /c ftype $type 2>$null } else { $null }
170
+ [PSCustomObject]@{
171
+ Extension = '${extension}'
172
+ FileType = if ($assoc) { ($assoc -split '=')[1] } else { 'Not associated' }
173
+ OpensWith = if ($ftype) { ($ftype -split '=')[1] } else { 'Unknown' }
174
+ } | ConvertTo-Json -Compress`;
175
+ const result = await runPowerShell(ps, { timeout: 10000 });
176
+ if (result.exitCode !== 0) {
177
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
178
+ }
179
+ const info = JSON.parse(result.stdout);
180
+ return { content: [{ type: 'text', text: `${info.Extension} -> ${info.FileType} -> ${info.OpensWith}` }] };
181
+ }
182
+
183
+ // List common defaults
184
+ const ps = `
185
+ $defaults = @('.html','.pdf','.txt','.jpg','.png','.mp3','.mp4','.zip') | ForEach-Object {
186
+ $ext = $_
187
+ $assoc = cmd /c assoc $ext 2>$null
188
+ $type = if ($assoc) { ($assoc -split '=')[1] } else { 'N/A' }
189
+ [PSCustomObject]@{ Extension = $ext; FileType = $type }
190
+ }
191
+ $defaults | ConvertTo-Json -Depth 3 -Compress`;
192
+
193
+ const result = await runPowerShell(ps, { timeout: 15000 });
194
+ if (result.exitCode !== 0) {
195
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
196
+ }
197
+
198
+ const defaults = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
199
+ const lines = defaults.map((d: { Extension: string; FileType: string }) =>
200
+ ` ${d.Extension.padEnd(8)} ${d.FileType}`,
201
+ );
202
+ return { content: [{ type: 'text', text: `Default file associations:\n${lines.join('\n')}` }] };
203
+ },
204
+ );
205
+ }
@@ -0,0 +1,65 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_usb_devices (#49)
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 registerUsbTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_usb_devices',
14
+ 'List connected USB devices with name, manufacturer, type, and status. Supports safe eject for storage.',
15
+ {
16
+ eject: z.string().optional().describe('Drive letter to safely eject (e.g. "E:")'),
17
+ },
18
+ async ({ eject }) => {
19
+ if (eject) {
20
+ const letter = eject.replace(':', '').toUpperCase();
21
+ const ps = `
22
+ $vol = Get-CimInstance Win32_Volume -Filter "DriveLetter='${letter}:'" -ErrorAction Stop
23
+ $ejectResult = $vol | Invoke-CimMethod -MethodName Dismount -Arguments @{Force=$false} -ErrorAction Stop
24
+ if ($ejectResult.ReturnValue -eq 0) { "Safely ejected ${letter}:" } else { "Eject failed (code: $($ejectResult.ReturnValue)). Close all files on the drive first." }`;
25
+
26
+ const result = await runPowerShell(ps, { timeout: 15000 });
27
+ return {
28
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
29
+ isError: result.exitCode !== 0,
30
+ };
31
+ }
32
+
33
+ const ps = `
34
+ Get-PnpDevice -Class USB -PresentOnly -ErrorAction SilentlyContinue | ForEach-Object {
35
+ [PSCustomObject]@{
36
+ Name = $_.FriendlyName
37
+ Status = $_.Status
38
+ Manufacturer = $_.Manufacturer
39
+ InstanceId = $_.InstanceId
40
+ Class = $_.Class
41
+ }
42
+ } | Where-Object { $_.Name } | Sort-Object Name | ConvertTo-Json -Depth 3 -Compress`;
43
+
44
+ const result = await runPowerShell(ps, { timeout: 15000 });
45
+ if (result.exitCode !== 0) {
46
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
47
+ }
48
+
49
+ if (!result.stdout) {
50
+ return { content: [{ type: 'text', text: 'No USB devices found.' }] };
51
+ }
52
+
53
+ const devices = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
54
+ const lines = devices.map((d: { Name: string; Status: string; Manufacturer: string }) => {
55
+ const status = d.Status === 'OK' ? '[OK]' : `[${d.Status?.slice(0, 3) || '?'}]`;
56
+ return `${status} ${(d.Name || '').padEnd(45).slice(0, 45)} ${(d.Manufacturer || '').slice(0, 25)}`;
57
+ });
58
+
59
+ const header = `Sta ${'Name'.padEnd(45)} Manufacturer`;
60
+ return {
61
+ content: [{ type: 'text', text: `${header}\n${'─'.repeat(80)}\n${lines.join('\n')}\n\n${devices.length} USB devices` }],
62
+ };
63
+ },
64
+ );
65
+ }
@@ -0,0 +1,122 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_virtual_desktop (#54)
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 registerVirtualDesktopTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_virtual_desktop',
14
+ 'List, create, switch, or remove virtual desktops.',
15
+ {
16
+ action: z.enum(['list', 'create', 'switch', 'remove']).default('list').describe('Action'),
17
+ index: z.number().optional().describe('Desktop index (0-based, for switch/remove)'),
18
+ },
19
+ async ({ action, index }) => {
20
+ switch (action) {
21
+ case 'list': {
22
+ const ps = `
23
+ $desktops = Get-CimInstance -Namespace root\\cimv2 -ClassName Win32_Desktop -ErrorAction SilentlyContinue
24
+ # Virtual desktops are best accessed via COM, but we can detect count from registry
25
+ $vdKey = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VirtualDesktops'
26
+ $ids = (Get-ItemProperty -Path $vdKey -Name VirtualDesktopIDs -ErrorAction SilentlyContinue).VirtualDesktopIDs
27
+ $currentId = (Get-ItemProperty -Path $vdKey -Name CurrentVirtualDesktop -ErrorAction SilentlyContinue).CurrentVirtualDesktop
28
+ $count = if ($ids) { $ids.Length / 16 } else { 1 }
29
+ [PSCustomObject]@{
30
+ Count = $count
31
+ CurrentIndex = 'Use Ctrl+Win+Left/Right to navigate'
32
+ Note = 'Windows does not expose virtual desktop names via public API'
33
+ } | ConvertTo-Json -Compress`;
34
+ const result = await runPowerShell(ps, { timeout: 10000 });
35
+ if (result.exitCode !== 0) {
36
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
37
+ }
38
+ const info = JSON.parse(result.stdout);
39
+ return { content: [{ type: 'text', text: `Virtual desktops: ${info.Count}\n${info.Note}` }] };
40
+ }
41
+
42
+ case 'create': {
43
+ // Use keyboard shortcut via SendKeys
44
+ const ps = `
45
+ Add-Type @'
46
+ using System;
47
+ using System.Runtime.InteropServices;
48
+ public class VDKeys {
49
+ [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
50
+ public static void NewDesktop() {
51
+ // Ctrl+Win+D
52
+ keybd_event(0x11, 0, 0, UIntPtr.Zero); // Ctrl down
53
+ keybd_event(0x5B, 0, 0, UIntPtr.Zero); // Win down
54
+ keybd_event(0x44, 0, 0, UIntPtr.Zero); // D down
55
+ keybd_event(0x44, 0, 2, UIntPtr.Zero); // D up
56
+ keybd_event(0x5B, 0, 2, UIntPtr.Zero); // Win up
57
+ keybd_event(0x11, 0, 2, UIntPtr.Zero); // Ctrl up
58
+ }
59
+ }
60
+ '@
61
+ [VDKeys]::NewDesktop()
62
+ Start-Sleep -Milliseconds 500
63
+ "New virtual desktop created (switched to it)"`;
64
+ const result = await runPowerShell(ps, { timeout: 10000 });
65
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
66
+ }
67
+
68
+ case 'switch': {
69
+ if (index === undefined) {
70
+ return { content: [{ type: 'text', text: 'Switch requires index.' }], isError: true };
71
+ }
72
+ // Navigate left/right to reach target
73
+ const ps = `
74
+ Add-Type @'
75
+ using System;
76
+ using System.Runtime.InteropServices;
77
+ public class VDSwitch {
78
+ [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
79
+ public static void Left() {
80
+ keybd_event(0x11, 0, 0, UIntPtr.Zero); keybd_event(0x5B, 0, 0, UIntPtr.Zero);
81
+ keybd_event(0x25, 0, 0, UIntPtr.Zero); keybd_event(0x25, 0, 2, UIntPtr.Zero);
82
+ keybd_event(0x5B, 0, 2, UIntPtr.Zero); keybd_event(0x11, 0, 2, UIntPtr.Zero);
83
+ }
84
+ public static void Right() {
85
+ keybd_event(0x11, 0, 0, UIntPtr.Zero); keybd_event(0x5B, 0, 0, UIntPtr.Zero);
86
+ keybd_event(0x27, 0, 0, UIntPtr.Zero); keybd_event(0x27, 0, 2, UIntPtr.Zero);
87
+ keybd_event(0x5B, 0, 2, UIntPtr.Zero); keybd_event(0x11, 0, 2, UIntPtr.Zero);
88
+ }
89
+ }
90
+ '@
91
+ # Go far left first, then right to target index
92
+ for ($i = 0; $i -lt 20; $i++) { [VDSwitch]::Left(); Start-Sleep -Milliseconds 100 }
93
+ for ($i = 0; $i -lt ${index}; $i++) { [VDSwitch]::Right(); Start-Sleep -Milliseconds 100 }
94
+ "Switched to desktop ${index}"`;
95
+ const result = await runPowerShell(ps, { timeout: 15000 });
96
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
97
+ }
98
+
99
+ case 'remove': {
100
+ const ps = `
101
+ Add-Type @'
102
+ using System;
103
+ using System.Runtime.InteropServices;
104
+ public class VDClose {
105
+ [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
106
+ public static void CloseDesktop() {
107
+ keybd_event(0x11, 0, 0, UIntPtr.Zero); keybd_event(0x5B, 0, 0, UIntPtr.Zero);
108
+ keybd_event(0x73, 0, 0, UIntPtr.Zero); // F4
109
+ keybd_event(0x73, 0, 2, UIntPtr.Zero);
110
+ keybd_event(0x5B, 0, 2, UIntPtr.Zero); keybd_event(0x11, 0, 2, UIntPtr.Zero);
111
+ }
112
+ }
113
+ '@
114
+ [VDClose]::CloseDesktop()
115
+ "Current virtual desktop removed"`;
116
+ const result = await runPowerShell(ps, { timeout: 10000 });
117
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
118
+ }
119
+ }
120
+ },
121
+ );
122
+ }
@@ -0,0 +1,157 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_wifi_networks (#47), windows_wifi_connect (#48)
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 registerWifiTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_wifi_networks',
14
+ 'Scan and list available Wi-Fi networks with signal strength, security, and saved profiles.',
15
+ {
16
+ rescan: z.boolean().default(false).describe('Force a rescan before listing'),
17
+ saved: z.boolean().default(false).describe('List saved profiles instead of available networks'),
18
+ },
19
+ async ({ rescan, saved }) => {
20
+ if (saved) {
21
+ const ps = `netsh wlan show profiles | Select-String 'All User Profile' | ForEach-Object { ($_ -split ':')[1].Trim() }`;
22
+ const result = await runPowerShell(ps, { timeout: 10000 });
23
+ if (result.exitCode !== 0) {
24
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
25
+ }
26
+ const profiles = result.stdout ? result.stdout.split('\n').filter(Boolean) : [];
27
+ return {
28
+ content: [{ type: 'text', text: `Saved Wi-Fi profiles (${profiles.length}):\n${profiles.map(p => ` ${p}`).join('\n')}` }],
29
+ };
30
+ }
31
+
32
+ const scanCmd = rescan ? `netsh wlan scan; Start-Sleep -Seconds 3;` : '';
33
+ const ps = `
34
+ ${scanCmd}
35
+ $output = netsh wlan show networks mode=bssid 2>&1
36
+ $networks = [System.Collections.Generic.List[PSObject]]::new()
37
+ $current = @{}
38
+
39
+ foreach ($line in ($output -split [char]10)) {
40
+ $line = $line.Trim()
41
+ if ($line -match '^SSID \\d+ : (.*)') {
42
+ if ($current.SSID) { $networks.Add([PSCustomObject]$current) }
43
+ $current = @{ SSID = $Matches[1]; Signal = ''; Auth = ''; Channel = ''; BSSID = '' }
44
+ }
45
+ elseif ($line -match '^Signal\\s+: (.*)') { $current.Signal = $Matches[1] }
46
+ elseif ($line -match '^Authentication\\s+: (.*)') { $current.Auth = $Matches[1] }
47
+ elseif ($line -match '^Channel\\s+: (.*)') { $current.Channel = $Matches[1] }
48
+ elseif ($line -match '^BSSID \\d+\\s+: (.*)') { $current.BSSID = $Matches[1] }
49
+ }
50
+ if ($current.SSID) { $networks.Add([PSCustomObject]$current) }
51
+
52
+ # Current connection
53
+ $connected = (netsh wlan show interfaces | Select-String 'SSID\\s+:' | Select-Object -First 1) -replace '.*:\\s*',''
54
+
55
+ $networks | Sort-Object { [int]($_.Signal -replace '%','') } -Descending | ConvertTo-Json -Depth 3 -Compress
56
+ Write-Output "---CONNECTED:$($connected.Trim())"`;
57
+
58
+ const result = await runPowerShell(ps, { timeout: 20000 });
59
+ if (result.exitCode !== 0) {
60
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
61
+ }
62
+
63
+ const outputLines = result.stdout.split('\n');
64
+ const connectedLine = outputLines.find(l => l.startsWith('---CONNECTED:'));
65
+ const connected = connectedLine ? connectedLine.replace('---CONNECTED:', '').trim() : '';
66
+ const jsonLine = outputLines.filter(l => !l.startsWith('---CONNECTED:')).join('\n');
67
+
68
+ let networks: Array<{ SSID: string; Signal: string; Auth: string; Channel: string }> = [];
69
+ if (jsonLine.trim()) {
70
+ try {
71
+ const parsed = JSON.parse(jsonLine);
72
+ networks = Array.isArray(parsed) ? parsed : [parsed];
73
+ } catch { /* empty */ }
74
+ }
75
+
76
+ const lines = networks.map(n => {
77
+ const icon = n.SSID === connected ? ' *' : ' ';
78
+ return `${icon} ${n.Signal.padStart(4)} ${n.Auth.padEnd(18)} Ch ${(n.Channel || '?').padEnd(3)} ${n.SSID}`;
79
+ });
80
+
81
+ const header = ` ${'Sig'.padStart(4)} ${'Security'.padEnd(18)} ${'Ch'.padEnd(5)} SSID`;
82
+ return {
83
+ content: [{
84
+ type: 'text',
85
+ text: `Connected: ${connected || '(none)'}\n\n${header}\n${'─'.repeat(65)}\n${lines.join('\n')}\n\n${networks.length} networks (* = connected)`,
86
+ }],
87
+ };
88
+ },
89
+ );
90
+
91
+ server.tool(
92
+ 'windows_wifi_connect',
93
+ 'Connect to a Wi-Fi network, disconnect, or forget a saved profile.',
94
+ {
95
+ action: z.enum(['connect', 'disconnect', 'forget']).describe('Action'),
96
+ ssid: z.string().optional().describe('Network SSID (for connect/forget)'),
97
+ password: z.string().optional().describe('Password (for connecting to a new network)'),
98
+ },
99
+ async ({ action, ssid, password }) => {
100
+ let ps: string;
101
+
102
+ switch (action) {
103
+ case 'disconnect':
104
+ ps = `netsh wlan disconnect; "Disconnected from Wi-Fi"`;
105
+ break;
106
+
107
+ case 'forget':
108
+ if (!ssid) {
109
+ return { content: [{ type: 'text', text: 'Forget requires ssid.' }], isError: true };
110
+ }
111
+ ps = `netsh wlan delete profile name="${ssid.replace(/"/g, '""')}" 2>&1; "Forgot network: ${ssid}"`;
112
+ break;
113
+
114
+ case 'connect':
115
+ if (!ssid) {
116
+ return { content: [{ type: 'text', text: 'Connect requires ssid.' }], isError: true };
117
+ }
118
+ // Check if profile exists
119
+ if (password) {
120
+ // Create a temporary profile XML for new networks
121
+ ps = `
122
+ $profileXml = @"
123
+ <?xml version="1.0"?>
124
+ <WLANProfile xmlns="http://www.microsoft.com/networking/WLAN/profile/v1">
125
+ <name>${ssid}</name>
126
+ <SSIDConfig><SSID><name>${ssid}</name></SSID></SSIDConfig>
127
+ <connectionType>ESS</connectionType>
128
+ <connectionMode>auto</connectionMode>
129
+ <MSM><security>
130
+ <authEncryption><authentication>WPA2PSK</authentication><encryption>AES</encryption><useOneX>false</useOneX></authEncryption>
131
+ <sharedKey><keyType>passPhrase</keyType><protected>false</protected><keyMaterial>${password}</keyMaterial></sharedKey>
132
+ </security></MSM>
133
+ </WLANProfile>
134
+ "@
135
+ $tempFile = [IO.Path]::GetTempFileName()
136
+ $profileXml | Out-File -Encoding UTF8 $tempFile
137
+ netsh wlan add profile filename="$tempFile" 2>&1 | Out-Null
138
+ Remove-Item $tempFile
139
+ netsh wlan connect name="${ssid.replace(/"/g, '""')}" 2>&1
140
+ Start-Sleep -Seconds 3
141
+ $iface = netsh wlan show interfaces
142
+ $connectedSsid = ($iface | Select-String 'SSID\\s+:' | Select-Object -First 1) -replace '.*:\\s*',''
143
+ if ($connectedSsid.Trim() -eq '${ssid}') { "Connected to ${ssid}" } else { "Connection attempt sent for ${ssid}" }`;
144
+ } else {
145
+ ps = `netsh wlan connect name="${ssid.replace(/"/g, '""')}" 2>&1; Start-Sleep -Seconds 2; "Connect attempt sent for ${ssid}"`;
146
+ }
147
+ break;
148
+ }
149
+
150
+ const result = await runPowerShell(ps, { timeout: 20000 });
151
+ return {
152
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
153
+ isError: result.exitCode !== 0,
154
+ };
155
+ },
156
+ );
157
+ }