@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,211 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_window_list (#14), windows_window_control (#15)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ const WIN32_TYPES = `
12
+ Add-Type @'
13
+ using System;
14
+ using System.Text;
15
+ using System.Collections.Generic;
16
+ using System.Runtime.InteropServices;
17
+
18
+ public class WindowManager {
19
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
20
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
21
+ [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
22
+ [DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd);
23
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
24
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
25
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
26
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
27
+ [DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
28
+ [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
29
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
30
+ [DllImport("user32.dll")] public static extern bool IsZoomed(IntPtr hWnd);
31
+ [DllImport("user32.dll")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
32
+
33
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
34
+
35
+ [StructLayout(LayoutKind.Sequential)]
36
+ public struct RECT { public int Left, Top, Right, Bottom; }
37
+
38
+ public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
39
+ public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
40
+ public const uint SWP_NOMOVE = 0x0002;
41
+ public const uint SWP_NOSIZE = 0x0001;
42
+ public const uint WM_CLOSE = 0x0010;
43
+ }
44
+ '@ -ErrorAction SilentlyContinue
45
+ `;
46
+
47
+ export function registerWindowTools(server: McpServer): void {
48
+ server.tool(
49
+ 'windows_window_list',
50
+ 'List all visible windows with title, PID, process name, position, size, and state.',
51
+ {
52
+ filter: z.string().optional().describe('Filter by window title (substring)'),
53
+ },
54
+ async ({ filter }) => {
55
+ const filterClause = filter
56
+ ? `| Where-Object { $_.Title -like '*${filter.replace(/'/g, "''")}*' }`
57
+ : '';
58
+
59
+ const ps = `
60
+ ${WIN32_TYPES}
61
+ $windows = [System.Collections.Generic.List[PSObject]]::new()
62
+ $zOrder = 0
63
+ [WindowManager]::EnumWindows({
64
+ param($hWnd, $lParam)
65
+ if (-not [WindowManager]::IsWindowVisible($hWnd)) { return $true }
66
+ $len = [WindowManager]::GetWindowTextLength($hWnd)
67
+ if ($len -eq 0) { return $true }
68
+ $sb = New-Object System.Text.StringBuilder($len + 1)
69
+ [WindowManager]::GetWindowText($hWnd, $sb, $sb.Capacity) | Out-Null
70
+ $title = $sb.ToString()
71
+ if (-not $title) { return $true }
72
+
73
+ $pid = [uint32]0
74
+ [WindowManager]::GetWindowThreadProcessId($hWnd, [ref]$pid) | Out-Null
75
+ $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
76
+
77
+ $rect = New-Object WindowManager+RECT
78
+ [WindowManager]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
79
+
80
+ $state = 'Normal'
81
+ if ([WindowManager]::IsIconic($hWnd)) { $state = 'Minimized' }
82
+ elseif ([WindowManager]::IsZoomed($hWnd)) { $state = 'Maximized' }
83
+
84
+ $script:windows.Add([PSCustomObject]@{
85
+ ZOrder = $script:zOrder++
86
+ Title = $title
87
+ PID = $pid
88
+ Process = if ($proc) { $proc.ProcessName } else { '?' }
89
+ X = $rect.Left
90
+ Y = $rect.Top
91
+ Width = $rect.Right - $rect.Left
92
+ Height = $rect.Bottom - $rect.Top
93
+ State = $state
94
+ })
95
+ return $true
96
+ }, [IntPtr]::Zero) | Out-Null
97
+
98
+ $windows ${filterClause} | ConvertTo-Json -Depth 3 -Compress`;
99
+
100
+ const result = await runPowerShell(ps, { timeout: 10000 });
101
+ if (result.exitCode !== 0) {
102
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
103
+ }
104
+
105
+ if (!result.stdout) {
106
+ return { content: [{ type: 'text', text: 'No visible windows found.' }] };
107
+ }
108
+
109
+ const windows = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
110
+ const lines = windows.map((w: { ZOrder: number; Title: string; PID: number; Process: string; X: number; Y: number; Width: number; Height: number; State: string }) => {
111
+ const stateIcon = w.State === 'Minimized' ? '[-]' : w.State === 'Maximized' ? '[+]' : '[ ]';
112
+ return `${stateIcon} ${String(w.PID).padStart(6)} ${w.Process.padEnd(20).slice(0, 20)} ${String(w.Width).padStart(5)}x${String(w.Height).padEnd(5)} (${w.X},${w.Y}) ${w.Title.slice(0, 60)}`;
113
+ });
114
+
115
+ const header = `Sta ${'PID'.padStart(6)} ${'Process'.padEnd(20)} ${'Size'.padStart(11)} Pos Title`;
116
+ return {
117
+ content: [{ type: 'text', text: `${header}\n${'─'.repeat(110)}\n${lines.join('\n')}\n\n${windows.length} windows` }],
118
+ };
119
+ },
120
+ );
121
+
122
+ server.tool(
123
+ 'windows_window_control',
124
+ 'Move, resize, minimize, maximize, restore, close, or focus a window.',
125
+ {
126
+ title: z.string().optional().describe('Window title (substring match)'),
127
+ pid: z.number().optional().describe('Process ID'),
128
+ action: z.enum(['minimize', 'maximize', 'restore', 'close', 'focus', 'move', 'resize', 'topmost']).describe('Action'),
129
+ x: z.number().optional().describe('X position (for move)'),
130
+ y: z.number().optional().describe('Y position (for move)'),
131
+ width: z.number().optional().describe('Width (for resize)'),
132
+ height: z.number().optional().describe('Height (for resize)'),
133
+ topmost: z.boolean().optional().describe('Set always-on-top (for topmost action)'),
134
+ },
135
+ async ({ title, pid, action, x, y, width, height, topmost }) => {
136
+ if (!title && !pid) {
137
+ return { content: [{ type: 'text', text: 'Provide either title or pid.' }], isError: true };
138
+ }
139
+
140
+ const findWindow = title
141
+ ? `
142
+ $target = '${title.replace(/'/g, "''")}'
143
+ $hWnd = [IntPtr]::Zero
144
+ [WindowManager]::EnumWindows({
145
+ param($h, $l)
146
+ $sb = New-Object System.Text.StringBuilder 256
147
+ [WindowManager]::GetWindowText($h, $sb, 256) | Out-Null
148
+ if ($sb.ToString() -like "*$target*" -and [WindowManager]::IsWindowVisible($h)) {
149
+ $script:hWnd = $h; return $false
150
+ }
151
+ return $true
152
+ }, [IntPtr]::Zero) | Out-Null
153
+ if ($hWnd -eq [IntPtr]::Zero) { throw "Window not found: $target" }`
154
+ : `
155
+ $proc = Get-Process -Id ${pid} -ErrorAction Stop
156
+ $hWnd = $proc.MainWindowHandle
157
+ if ($hWnd -eq [IntPtr]::Zero) { throw "Process ${pid} has no visible window" }`;
158
+
159
+ let actionCode: string;
160
+ switch (action) {
161
+ case 'minimize':
162
+ actionCode = `[WindowManager]::ShowWindow($hWnd, 6) | Out-Null; "Minimized"`;
163
+ break;
164
+ case 'maximize':
165
+ actionCode = `[WindowManager]::ShowWindow($hWnd, 3) | Out-Null; "Maximized"`;
166
+ break;
167
+ case 'restore':
168
+ actionCode = `[WindowManager]::ShowWindow($hWnd, 9) | Out-Null; "Restored"`;
169
+ break;
170
+ case 'close':
171
+ actionCode = `[WindowManager]::PostMessage($hWnd, [WindowManager]::WM_CLOSE, [IntPtr]::Zero, [IntPtr]::Zero) | Out-Null; "Close message sent"`;
172
+ break;
173
+ case 'focus':
174
+ actionCode = `[WindowManager]::ShowWindow($hWnd, 9) | Out-Null; [WindowManager]::SetForegroundWindow($hWnd) | Out-Null; "Focused"`;
175
+ break;
176
+ case 'move':
177
+ if (x === undefined || y === undefined) {
178
+ return { content: [{ type: 'text', text: 'Move requires x and y.' }], isError: true };
179
+ }
180
+ actionCode = `
181
+ $rect = New-Object WindowManager+RECT
182
+ [WindowManager]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
183
+ $w = $rect.Right - $rect.Left; $h = $rect.Bottom - $rect.Top
184
+ [WindowManager]::MoveWindow($hWnd, ${x}, ${y}, $w, $h, $true) | Out-Null
185
+ "Moved to (${x}, ${y})"`;
186
+ break;
187
+ case 'resize':
188
+ if (!width || !height) {
189
+ return { content: [{ type: 'text', text: 'Resize requires width and height.' }], isError: true };
190
+ }
191
+ actionCode = `
192
+ $rect = New-Object WindowManager+RECT
193
+ [WindowManager]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
194
+ [WindowManager]::MoveWindow($hWnd, $rect.Left, $rect.Top, ${width}, ${height}, $true) | Out-Null
195
+ "Resized to ${width}x${height}"`;
196
+ break;
197
+ case 'topmost':
198
+ const insertAfter = topmost !== false ? '[WindowManager]::HWND_TOPMOST' : '[WindowManager]::HWND_NOTOPMOST';
199
+ actionCode = `[WindowManager]::SetWindowPos($hWnd, ${insertAfter}, 0, 0, 0, 0, [WindowManager]::SWP_NOMOVE -bor [WindowManager]::SWP_NOSIZE) | Out-Null; "Topmost: ${topmost !== false}"`;
200
+ break;
201
+ }
202
+
203
+ const ps = `${WIN32_TYPES}\n${findWindow}\n${actionCode}`;
204
+ const result = await runPowerShell(ps, { timeout: 10000 });
205
+ return {
206
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
207
+ isError: result.exitCode !== 0,
208
+ };
209
+ },
210
+ );
211
+ }
@@ -0,0 +1,100 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_winget_search (#69), windows_winget_manage (#70), windows_winget_export (#71)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runShell } from '../shell.js';
10
+
11
+ export function registerWingetTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_winget_search',
14
+ 'Search for packages in the winget repository.',
15
+ {
16
+ query: z.string().describe('Search query (name or ID)'),
17
+ source: z.enum(['winget', 'msstore', 'all']).default('all').describe('Package source'),
18
+ limit: z.number().default(20).describe('Max results'),
19
+ },
20
+ async ({ query, source, limit }) => {
21
+ const sourceArg = source !== 'all' ? `--source ${source}` : '';
22
+ const cmd = `winget search "${query}" ${sourceArg} --count ${limit} --accept-source-agreements --disable-interactivity`;
23
+ const result = await runShell(cmd, { shell: 'cmd', timeout: 30000 });
24
+ const clean = result.stdout.replace(/\0/g, '').trim();
25
+ return {
26
+ content: [{ type: 'text', text: clean || 'No packages found.' }],
27
+ isError: result.exitCode !== 0 && !clean,
28
+ };
29
+ },
30
+ );
31
+
32
+ server.tool(
33
+ 'windows_winget_manage',
34
+ 'Install, upgrade, or uninstall packages via winget. Also list installed or upgradable packages.',
35
+ {
36
+ action: z.enum(['install', 'upgrade', 'uninstall', 'list', 'upgradable', 'upgrade_all']).describe('Action'),
37
+ id: z.string().optional().describe('Package ID (e.g. "Microsoft.VisualStudioCode")'),
38
+ version: z.string().optional().describe('Specific version to install'),
39
+ },
40
+ async ({ action, id, version }) => {
41
+ let cmd: string;
42
+
43
+ switch (action) {
44
+ case 'install':
45
+ if (!id) return { content: [{ type: 'text', text: 'install requires id.' }], isError: true };
46
+ cmd = `winget install --id "${id}" ${version ? `--version "${version}"` : ''} --accept-package-agreements --accept-source-agreements --disable-interactivity`;
47
+ break;
48
+ case 'upgrade':
49
+ if (!id) return { content: [{ type: 'text', text: 'upgrade requires id.' }], isError: true };
50
+ cmd = `winget upgrade --id "${id}" --accept-package-agreements --accept-source-agreements --disable-interactivity`;
51
+ break;
52
+ case 'upgrade_all':
53
+ cmd = `winget upgrade --all --accept-package-agreements --accept-source-agreements --disable-interactivity`;
54
+ break;
55
+ case 'uninstall':
56
+ if (!id) return { content: [{ type: 'text', text: 'uninstall requires id.' }], isError: true };
57
+ cmd = `winget uninstall --id "${id}" --disable-interactivity`;
58
+ break;
59
+ case 'list':
60
+ cmd = `winget list --accept-source-agreements --disable-interactivity`;
61
+ break;
62
+ case 'upgradable':
63
+ cmd = `winget upgrade --accept-source-agreements --disable-interactivity`;
64
+ break;
65
+ }
66
+
67
+ const timeout = action === 'install' || action === 'upgrade' || action === 'upgrade_all' ? 300000 : 30000;
68
+ const result = await runShell(cmd, { shell: 'cmd', timeout });
69
+ const clean = result.stdout.replace(/\0/g, '').trim();
70
+
71
+ return {
72
+ content: [{ type: 'text', text: clean || result.stderr || `${action} completed.` }],
73
+ isError: result.exitCode !== 0 && !clean,
74
+ };
75
+ },
76
+ );
77
+
78
+ server.tool(
79
+ 'windows_winget_export',
80
+ 'Export installed packages to JSON or import from a JSON file.',
81
+ {
82
+ action: z.enum(['export', 'import']).describe('Action'),
83
+ path: z.string().describe('File path for export/import JSON'),
84
+ },
85
+ async ({ action, path }) => {
86
+ const cmd = action === 'export'
87
+ ? `winget export -o "${path}" --accept-source-agreements --disable-interactivity`
88
+ : `winget import -i "${path}" --accept-package-agreements --accept-source-agreements --disable-interactivity`;
89
+
90
+ const timeout = action === 'import' ? 600000 : 30000;
91
+ const result = await runShell(cmd, { shell: 'cmd', timeout });
92
+ const clean = result.stdout.replace(/\0/g, '').trim();
93
+
94
+ return {
95
+ content: [{ type: 'text', text: clean || `${action} completed: ${path}` }],
96
+ isError: result.exitCode !== 0 && !clean,
97
+ };
98
+ },
99
+ );
100
+ }
@@ -0,0 +1,112 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_wsl_list (#66), windows_wsl_manage (#67), windows_wsl_exec (#68)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runShell, runPowerShell } from '../shell.js';
10
+
11
+ export function registerWslTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_wsl_list',
14
+ 'List installed WSL distributions with status, version, and default flag.',
15
+ {},
16
+ async () => {
17
+ // wsl.exe outputs UTF-16; use PowerShell to decode properly
18
+ const ps = `$out = wsl --list --verbose 2>&1; if ($LASTEXITCODE -ne 0) { "WSL_ERROR:$out" } else { $out }`;
19
+ const result = await runPowerShell(ps, { timeout: 15000 });
20
+ const clean = result.stdout.replace(/\0/g, '').trim();
21
+ if (clean.startsWith('WSL_ERROR:') || result.exitCode !== 0) {
22
+ const msg = clean.replace('WSL_ERROR:', '').trim();
23
+ return { content: [{ type: 'text', text: msg || 'WSL is not installed or not available. Install with: wsl --install' }] };
24
+ }
25
+ return { content: [{ type: 'text', text: clean || 'No WSL distributions installed.' }] };
26
+ },
27
+ );
28
+
29
+ server.tool(
30
+ 'windows_wsl_manage',
31
+ 'Manage WSL: start, stop, shutdown, set default, export, import, or remove distros.',
32
+ {
33
+ action: z.enum(['shutdown', 'terminate', 'set_default', 'set_version', 'export', 'import', 'unregister']).describe('Action'),
34
+ distro: z.string().optional().describe('Distribution name'),
35
+ version: z.number().optional().describe('WSL version 1 or 2 (for set_version)'),
36
+ path: z.string().optional().describe('File path (for export/import)'),
37
+ },
38
+ async ({ action, distro, version, path }) => {
39
+ let cmd: string;
40
+
41
+ switch (action) {
42
+ case 'shutdown':
43
+ cmd = 'wsl --shutdown';
44
+ break;
45
+ case 'terminate':
46
+ if (!distro) return { content: [{ type: 'text', text: 'terminate requires distro.' }], isError: true };
47
+ cmd = `wsl --terminate ${distro}`;
48
+ break;
49
+ case 'set_default':
50
+ if (!distro) return { content: [{ type: 'text', text: 'set_default requires distro.' }], isError: true };
51
+ cmd = `wsl --set-default ${distro}`;
52
+ break;
53
+ case 'set_version':
54
+ if (!distro || !version) return { content: [{ type: 'text', text: 'set_version requires distro and version.' }], isError: true };
55
+ cmd = `wsl --set-version ${distro} ${version}`;
56
+ break;
57
+ case 'export':
58
+ if (!distro || !path) return { content: [{ type: 'text', text: 'export requires distro and path.' }], isError: true };
59
+ cmd = `wsl --export ${distro} "${path}"`;
60
+ break;
61
+ case 'import':
62
+ if (!distro || !path) return { content: [{ type: 'text', text: 'import requires distro and path.' }], isError: true };
63
+ cmd = `wsl --import ${distro} "C:\\WSL\\${distro}" "${path}"`;
64
+ break;
65
+ case 'unregister':
66
+ if (!distro) return { content: [{ type: 'text', text: 'unregister requires distro.' }], isError: true };
67
+ cmd = `wsl --unregister ${distro}`;
68
+ break;
69
+ }
70
+
71
+ const result = await runShell(cmd, { shell: 'cmd', timeout: 60000 });
72
+ const clean = (result.stdout + '\n' + result.stderr).replace(/\0/g, '').trim();
73
+ return {
74
+ content: [{ type: 'text', text: clean || `${action} completed.` }],
75
+ isError: result.exitCode !== 0,
76
+ };
77
+ },
78
+ );
79
+
80
+ server.tool(
81
+ 'windows_wsl_exec',
82
+ 'Execute a command inside a WSL distribution. Returns stdout, stderr, exit code.',
83
+ {
84
+ command: z.string().describe('Command to execute'),
85
+ distro: z.string().optional().describe('Distribution name (default distro if omitted)'),
86
+ user: z.string().optional().describe('Run as user'),
87
+ cwd: z.string().optional().describe('Working directory inside WSL'),
88
+ timeout: z.number().default(30000).describe('Timeout in ms'),
89
+ },
90
+ async ({ command, distro, user, cwd, timeout }) => {
91
+ const parts = ['wsl'];
92
+ if (distro) parts.push('-d', distro);
93
+ if (user) parts.push('-u', user);
94
+ if (cwd) parts.push('--cd', cwd);
95
+ parts.push('--', 'bash', '-c', `"${command.replace(/"/g, '\\"')}"`);
96
+
97
+ const result = await runShell(parts.join(' '), { shell: 'cmd', timeout });
98
+ const clean = result.stdout.replace(/\0/g, '').trim();
99
+ const cleanErr = result.stderr.replace(/\0/g, '').trim();
100
+
101
+ const output: string[] = [];
102
+ if (clean) output.push(clean);
103
+ if (cleanErr) output.push(`[stderr]\n${cleanErr}`);
104
+ output.push(`[exit code: ${result.exitCode}]`);
105
+
106
+ return {
107
+ content: [{ type: 'text', text: output.join('\n\n') }],
108
+ isError: result.exitCode !== 0,
109
+ };
110
+ },
111
+ );
112
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }