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