@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,261 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_shortcut (#78), windows_input (#79),
5
+ * windows_font_list (#80), windows_sandbox (#81), windows_screen_capture (#82)
6
+ */
7
+ import { z } from 'zod';
8
+ import { runPowerShell } from '../shell.js';
9
+ export function registerAutomationTools(server) {
10
+ server.tool('windows_shortcut', 'Create, read, or modify Windows shortcut (.lnk) files.', {
11
+ action: z.enum(['create', 'read']).describe('Action'),
12
+ path: z.string().describe('Shortcut .lnk file path'),
13
+ target: z.string().optional().describe('Target path (for create)'),
14
+ arguments: z.string().optional().describe('Arguments (for create)'),
15
+ icon: z.string().optional().describe('Icon path (for create)'),
16
+ working_dir: z.string().optional().describe('Working directory (for create)'),
17
+ description: z.string().optional().describe('Description (for create)'),
18
+ }, async ({ action, path, target, arguments: args, icon, working_dir, description }) => {
19
+ if (action === 'read') {
20
+ const ps = `
21
+ $ws = New-Object -ComObject WScript.Shell
22
+ $sc = $ws.CreateShortcut('${path.replace(/'/g, "''")}')
23
+ [PSCustomObject]@{
24
+ Target = $sc.TargetPath
25
+ Arguments = $sc.Arguments
26
+ WorkingDir = $sc.WorkingDirectory
27
+ Icon = $sc.IconLocation
28
+ Description = $sc.Description
29
+ Hotkey = $sc.Hotkey
30
+ WindowStyle = $sc.WindowStyle
31
+ } | ConvertTo-Json -Compress`;
32
+ const result = await runPowerShell(ps, { timeout: 10000 });
33
+ if (result.exitCode !== 0)
34
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
35
+ const sc = JSON.parse(result.stdout);
36
+ return {
37
+ content: [{
38
+ type: 'text',
39
+ text: `${path}\n Target: ${sc.Target}\n Args: ${sc.Arguments || '(none)'}\n Dir: ${sc.WorkingDir || '(none)'}\n Icon: ${sc.Icon || '(default)'}\n Description: ${sc.Description || '(none)'}`,
40
+ }],
41
+ };
42
+ }
43
+ if (!target)
44
+ return { content: [{ type: 'text', text: 'create requires target.' }], isError: true };
45
+ const ps = `
46
+ $ws = New-Object -ComObject WScript.Shell
47
+ $sc = $ws.CreateShortcut('${path.replace(/'/g, "''")}')
48
+ $sc.TargetPath = '${target.replace(/'/g, "''")}'
49
+ ${args ? `$sc.Arguments = '${args.replace(/'/g, "''")}'` : ''}
50
+ ${working_dir ? `$sc.WorkingDirectory = '${working_dir.replace(/'/g, "''")}'` : ''}
51
+ ${icon ? `$sc.IconLocation = '${icon.replace(/'/g, "''")}'` : ''}
52
+ ${description ? `$sc.Description = '${description.replace(/'/g, "''")}'` : ''}
53
+ $sc.Save()
54
+ "Shortcut created: ${path}"`;
55
+ const result = await runPowerShell(ps, { timeout: 10000 });
56
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
57
+ });
58
+ server.tool('windows_input', 'Simulate keyboard presses, key combos, or type text. Also simulate mouse clicks and movement.', {
59
+ type: z.enum(['key', 'combo', 'text', 'mouse_click', 'mouse_move']).describe('Input type'),
60
+ key: z.string().optional().describe('Key name for key/combo (e.g. "Enter", "Ctrl+C", "Alt+F4")'),
61
+ text: z.string().optional().describe('Text to type (for text type)'),
62
+ x: z.number().optional().describe('Mouse X coordinate'),
63
+ y: z.number().optional().describe('Mouse Y coordinate'),
64
+ button: z.enum(['left', 'right', 'middle']).default('left').describe('Mouse button'),
65
+ delay: z.number().default(50).describe('Delay between actions in ms'),
66
+ }, async ({ type, key, text, x, y, button, delay }) => {
67
+ let ps;
68
+ switch (type) {
69
+ case 'key':
70
+ if (!key)
71
+ return { content: [{ type: 'text', text: 'key requires key name.' }], isError: true };
72
+ ps = `
73
+ Add-Type -AssemblyName System.Windows.Forms
74
+ [System.Windows.Forms.SendKeys]::SendWait('{${key.toUpperCase()}}')
75
+ "Sent key: ${key}"`;
76
+ break;
77
+ case 'combo':
78
+ if (!key)
79
+ return { content: [{ type: 'text', text: 'combo requires key.' }], isError: true };
80
+ // Map Ctrl+C style to SendKeys format: ^c
81
+ const mapped = key
82
+ .replace(/Ctrl\+/gi, '^')
83
+ .replace(/Alt\+/gi, '%')
84
+ .replace(/Shift\+/gi, '+')
85
+ .replace(/Win\+/gi, '^{ESC}'); // approximate
86
+ ps = `
87
+ Add-Type -AssemblyName System.Windows.Forms
88
+ [System.Windows.Forms.SendKeys]::SendWait('${mapped}')
89
+ "Sent combo: ${key}"`;
90
+ break;
91
+ case 'text':
92
+ if (!text)
93
+ return { content: [{ type: 'text', text: 'text requires text.' }], isError: true };
94
+ // Escape SendKeys special chars
95
+ const escaped = text.replace(/[+^%~(){}[\]]/g, '{$&}');
96
+ ps = `
97
+ Add-Type -AssemblyName System.Windows.Forms
98
+ [System.Windows.Forms.SendKeys]::SendWait('${escaped.replace(/'/g, "''")}')
99
+ "Typed ${text.length} characters"`;
100
+ break;
101
+ case 'mouse_click':
102
+ if (x === undefined || y === undefined)
103
+ return { content: [{ type: 'text', text: 'mouse_click requires x and y.' }], isError: true };
104
+ const btnFlag = button === 'right' ? '0x0008,0x0010' : button === 'middle' ? '0x0020,0x0040' : '0x0002,0x0004';
105
+ ps = `
106
+ Add-Type @'
107
+ using System.Runtime.InteropServices;
108
+ public class MouseInput {
109
+ [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
110
+ [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, int dwExtraInfo);
111
+ }
112
+ '@
113
+ [MouseInput]::SetCursorPos(${x}, ${y})
114
+ Start-Sleep -Milliseconds ${delay}
115
+ [MouseInput]::mouse_event(${btnFlag.split(',')[0]}, 0, 0, 0, 0)
116
+ [MouseInput]::mouse_event(${btnFlag.split(',')[1]}, 0, 0, 0, 0)
117
+ "Clicked ${button} at (${x}, ${y})"`;
118
+ break;
119
+ case 'mouse_move':
120
+ if (x === undefined || y === undefined)
121
+ return { content: [{ type: 'text', text: 'mouse_move requires x and y.' }], isError: true };
122
+ ps = `
123
+ Add-Type @'
124
+ using System.Runtime.InteropServices;
125
+ public class MouseMove { [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); }
126
+ '@
127
+ [MouseMove]::SetCursorPos(${x}, ${y})
128
+ "Moved mouse to (${x}, ${y})"`;
129
+ break;
130
+ }
131
+ const result = await runPowerShell(ps, { timeout: 10000 });
132
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
133
+ });
134
+ server.tool('windows_font_list', 'List installed system and user fonts.', {
135
+ filter: z.string().optional().describe('Filter by font name'),
136
+ }, async ({ filter }) => {
137
+ const filterClause = filter ? `| Where-Object { $_.Name -like '*${filter.replace(/'/g, "''")}*' }` : '';
138
+ const ps = `
139
+ Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts' |
140
+ ForEach-Object { $_.PSObject.Properties } |
141
+ Where-Object { $_.Name -notlike 'PS*' } |
142
+ ForEach-Object { [PSCustomObject]@{ Name = $_.Name; File = $_.Value } } ${filterClause} |
143
+ Sort-Object Name |
144
+ ConvertTo-Json -Depth 3 -Compress`;
145
+ const result = await runPowerShell(ps, { timeout: 15000 });
146
+ if (result.exitCode !== 0)
147
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
148
+ if (!result.stdout)
149
+ return { content: [{ type: 'text', text: 'No fonts found.' }] };
150
+ const fonts = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
151
+ const lines = fonts.map((f) => ` ${f.Name}`);
152
+ return { content: [{ type: 'text', text: `Installed fonts (${fonts.length}):\n${lines.join('\n')}` }] };
153
+ });
154
+ server.tool('windows_sandbox', 'Check Windows Sandbox status, launch with default or custom config.', {
155
+ action: z.enum(['status', 'launch', 'generate_config']).default('status').describe('Action'),
156
+ mapped_folder: z.string().optional().describe('Host folder to map into sandbox'),
157
+ read_only: z.boolean().default(true).describe('Map folder as read-only'),
158
+ networking: z.boolean().default(true).describe('Enable networking in sandbox'),
159
+ startup_command: z.string().optional().describe('Command to run on sandbox startup'),
160
+ config_path: z.string().optional().describe('Path to save/load .wsb config'),
161
+ }, async ({ action, mapped_folder, read_only, networking, startup_command, config_path }) => {
162
+ if (action === 'status') {
163
+ const ps = `
164
+ $feature = Get-WindowsOptionalFeature -Online -FeatureName 'Containers-DisposableClientVM' -ErrorAction SilentlyContinue
165
+ if ($feature) {
166
+ "Windows Sandbox: $($feature.State)"
167
+ } else {
168
+ "Windows Sandbox feature not found"
169
+ }`;
170
+ const result = await runPowerShell(ps, { timeout: 15000 });
171
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
172
+ }
173
+ if (action === 'generate_config' || action === 'launch') {
174
+ const wsbLines = ['<Configuration>'];
175
+ if (!networking)
176
+ wsbLines.push(' <Networking>Disable</Networking>');
177
+ if (mapped_folder) {
178
+ wsbLines.push(' <MappedFolders>');
179
+ wsbLines.push(' <MappedFolder>');
180
+ wsbLines.push(` <HostFolder>${mapped_folder}</HostFolder>`);
181
+ wsbLines.push(` <ReadOnly>${read_only ? 'true' : 'false'}</ReadOnly>`);
182
+ wsbLines.push(' </MappedFolder>');
183
+ wsbLines.push(' </MappedFolders>');
184
+ }
185
+ if (startup_command) {
186
+ wsbLines.push(` <LogonCommand><Command>${startup_command}</Command></LogonCommand>`);
187
+ }
188
+ wsbLines.push('</Configuration>');
189
+ const wsb = wsbLines.join('\n');
190
+ if (action === 'generate_config') {
191
+ if (config_path) {
192
+ const ps = `Set-Content -Path '${config_path.replace(/'/g, "''")}' -Value @'\n${wsb}\n'@\n"Config saved: ${config_path}"`;
193
+ const result = await runPowerShell(ps, { timeout: 5000 });
194
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
195
+ }
196
+ return { content: [{ type: 'text', text: wsb }] };
197
+ }
198
+ // Launch
199
+ const tempWsb = config_path || `$env:TEMP\\mcp_sandbox_${Date.now()}.wsb`;
200
+ const ps = `
201
+ Set-Content -Path '${tempWsb}' -Value @'
202
+ ${wsb}
203
+ '@
204
+ Start-Process '${tempWsb}'
205
+ "Sandbox launched${config_path ? '' : ' (temp config)'}"`;
206
+ const result = await runPowerShell(ps, { timeout: 10000 });
207
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
208
+ }
209
+ return { content: [{ type: 'text', text: 'Unknown action.' }], isError: true };
210
+ });
211
+ server.tool('windows_screen_capture', 'Start or stop screen recording. Uses ffmpeg if available, otherwise reports status only.', {
212
+ action: z.enum(['start', 'stop', 'status']).describe('Action'),
213
+ output: z.string().optional().describe('Output file path (for start)'),
214
+ fps: z.number().default(15).describe('Framerate'),
215
+ }, async ({ action, output, fps }) => {
216
+ if (action === 'status') {
217
+ const ps = `
218
+ $ffmpeg = Get-Command ffmpeg -ErrorAction SilentlyContinue
219
+ $recording = Get-Process ffmpeg -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*gdigrab*' }
220
+ [PSCustomObject]@{
221
+ FfmpegInstalled = $null -ne $ffmpeg
222
+ FfmpegPath = if ($ffmpeg) { $ffmpeg.Source } else { $null }
223
+ ActiveRecording = $null -ne $recording
224
+ RecordingPID = if ($recording) { $recording.Id } else { $null }
225
+ } | ConvertTo-Json -Compress`;
226
+ const result = await runPowerShell(ps, { timeout: 10000 });
227
+ if (result.exitCode !== 0)
228
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
229
+ const info = JSON.parse(result.stdout);
230
+ return {
231
+ content: [{
232
+ type: 'text',
233
+ text: `ffmpeg: ${info.FfmpegInstalled ? info.FfmpegPath : 'Not installed (install via winget: winget install Gyan.FFmpeg)'}\nRecording: ${info.ActiveRecording ? `Active (PID ${info.RecordingPID})` : 'None'}`,
234
+ }],
235
+ };
236
+ }
237
+ if (action === 'start') {
238
+ const outPath = output || `A:/temp/recording_${Date.now()}.mp4`;
239
+ const ps = `
240
+ $ffmpeg = Get-Command ffmpeg -ErrorAction SilentlyContinue
241
+ if (-not $ffmpeg) { throw "ffmpeg not installed. Install with: winget install Gyan.FFmpeg" }
242
+ Start-Process ffmpeg -ArgumentList '-f gdigrab -framerate ${fps} -i desktop -c:v libx264 -preset ultrafast "${outPath}"' -WindowStyle Hidden -PassThru | ForEach-Object { "Recording started (PID $($_.Id)). Output: ${outPath}" }`;
243
+ const result = await runPowerShell(ps, { timeout: 10000 });
244
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
245
+ }
246
+ if (action === 'stop') {
247
+ const ps = `
248
+ $procs = Get-Process ffmpeg -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*gdigrab*' -or $_.CommandLine -eq $null }
249
+ if ($procs) {
250
+ $procs | ForEach-Object { $_.CloseMainWindow() | Out-Null }
251
+ Start-Sleep -Seconds 2
252
+ $procs | Stop-Process -Force -ErrorAction SilentlyContinue
253
+ "Recording stopped ($(@($procs).Count) process(es))"
254
+ } else { "No active recording found" }`;
255
+ const result = await runPowerShell(ps, { timeout: 10000 });
256
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
257
+ }
258
+ return { content: [{ type: 'text', text: 'Unknown action.' }], isError: true };
259
+ });
260
+ }
261
+ //# sourceMappingURL=automation.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerBluetoothTools(server: McpServer): void;
3
+ //# sourceMappingURL=bluetooth.d.ts.map
@@ -0,0 +1,96 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_bluetooth_get (#45), windows_bluetooth_control (#46)
5
+ */
6
+ import { z } from 'zod';
7
+ import { runPowerShell } from '../shell.js';
8
+ export function registerBluetoothTools(server) {
9
+ server.tool('windows_bluetooth_get', 'Get Bluetooth adapter status and list paired/connected devices.', {}, async () => {
10
+ const ps = `
11
+ $adapter = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -match 'Bluetooth' -and $_.Class -eq 'Bluetooth' } | Select-Object -First 1
12
+ $enabled = if ($adapter) { $adapter.Status -eq 'OK' } else { $false }
13
+
14
+ $devices = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue |
15
+ Where-Object { $_.FriendlyName -and $_.Class -eq 'Bluetooth' -and $_.FriendlyName -notmatch 'Bluetooth|Radio|Enumerator' } |
16
+ ForEach-Object {
17
+ [PSCustomObject]@{
18
+ Name = $_.FriendlyName
19
+ Status = $_.Status
20
+ InstanceId = $_.InstanceId
21
+ Connected = $_.Status -eq 'OK'
22
+ }
23
+ }
24
+
25
+ [PSCustomObject]@{
26
+ AdapterPresent = $null -ne $adapter
27
+ AdapterName = if ($adapter) { $adapter.FriendlyName } else { 'None' }
28
+ Enabled = $enabled
29
+ Devices = $devices
30
+ } | ConvertTo-Json -Depth 4 -Compress`;
31
+ const result = await runPowerShell(ps, { timeout: 15000 });
32
+ if (result.exitCode !== 0) {
33
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
34
+ }
35
+ const info = JSON.parse(result.stdout);
36
+ const lines = [
37
+ `Bluetooth: ${info.AdapterPresent ? (info.Enabled ? 'ON' : 'OFF') : 'Not available'}`,
38
+ `Adapter: ${info.AdapterName}`,
39
+ ];
40
+ const devices = Array.isArray(info.Devices) ? info.Devices : info.Devices ? [info.Devices] : [];
41
+ if (devices.length > 0) {
42
+ lines.push('', 'Paired Devices:');
43
+ for (const d of devices) {
44
+ const icon = d.Connected ? '[CON]' : '[ ]';
45
+ lines.push(` ${icon} ${d.Name}`);
46
+ }
47
+ }
48
+ else {
49
+ lines.push('', 'No paired devices.');
50
+ }
51
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
52
+ });
53
+ server.tool('windows_bluetooth_control', 'Enable/disable Bluetooth adapter or disconnect a device.', {
54
+ action: z.enum(['enable', 'disable', 'disconnect']).describe('Action'),
55
+ device: z.string().optional().describe('Device name (for disconnect)'),
56
+ }, async ({ action, device }) => {
57
+ let ps;
58
+ switch (action) {
59
+ case 'enable':
60
+ ps = `
61
+ $adapter = Get-PnpDevice -Class Bluetooth -ErrorAction Stop | Where-Object { $_.FriendlyName -match 'Bluetooth' -and $_.Class -eq 'Bluetooth' } | Select-Object -First 1
62
+ if ($adapter) {
63
+ Enable-PnpDevice -InstanceId $adapter.InstanceId -Confirm:$false -ErrorAction Stop
64
+ "Bluetooth enabled"
65
+ } else { "No Bluetooth adapter found" }`;
66
+ break;
67
+ case 'disable':
68
+ ps = `
69
+ $adapter = Get-PnpDevice -Class Bluetooth -ErrorAction Stop | Where-Object { $_.FriendlyName -match 'Bluetooth' -and $_.Class -eq 'Bluetooth' } | Select-Object -First 1
70
+ if ($adapter) {
71
+ Disable-PnpDevice -InstanceId $adapter.InstanceId -Confirm:$false -ErrorAction Stop
72
+ "Bluetooth disabled"
73
+ } else { "No Bluetooth adapter found" }`;
74
+ break;
75
+ case 'disconnect':
76
+ if (!device) {
77
+ return { content: [{ type: 'text', text: 'Disconnect requires device name.' }], isError: true };
78
+ }
79
+ ps = `
80
+ $dev = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -like '*${device.replace(/'/g, "''")}*' } | Select-Object -First 1
81
+ if ($dev) {
82
+ Disable-PnpDevice -InstanceId $dev.InstanceId -Confirm:$false -ErrorAction Stop
83
+ Start-Sleep -Seconds 1
84
+ Enable-PnpDevice -InstanceId $dev.InstanceId -Confirm:$false -ErrorAction Stop
85
+ "Disconnected and re-enabled: $($dev.FriendlyName)"
86
+ } else { "Device not found: ${device}" }`;
87
+ break;
88
+ }
89
+ const result = await runPowerShell(ps, { timeout: 15000 });
90
+ return {
91
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
92
+ isError: result.exitCode !== 0,
93
+ };
94
+ });
95
+ }
96
+ //# sourceMappingURL=bluetooth.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerClipboardTools(server: McpServer): void;
3
+ //# sourceMappingURL=clipboard.d.ts.map
@@ -0,0 +1,118 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_clipboard_get (#16), windows_clipboard_set (#17)
5
+ */
6
+ import { z } from 'zod';
7
+ import { runPowerShell } from '../shell.js';
8
+ export function registerClipboardTools(server) {
9
+ server.tool('windows_clipboard_get', 'Read clipboard contents: text, file list, or image (returned as base64).', {}, async () => {
10
+ // PowerShell must run in STA mode for clipboard access
11
+ const ps = `
12
+ powershell.exe -NoProfile -STA -Command {
13
+ Add-Type -AssemblyName System.Windows.Forms
14
+ $data = [System.Windows.Forms.Clipboard]::GetDataObject()
15
+ if (-not $data) { Write-Output '{"type":"empty","content":"Clipboard is empty"}'; return }
16
+
17
+ # Check for files
18
+ if ($data.ContainsFileDropList()) {
19
+ $files = [System.Windows.Forms.Clipboard]::GetFileDropList()
20
+ $list = @()
21
+ foreach ($f in $files) { $list += $f }
22
+ $json = @{ type = 'files'; content = $list } | ConvertTo-Json -Compress
23
+ Write-Output $json
24
+ return
25
+ }
26
+
27
+ # Check for image
28
+ if ($data.ContainsImage()) {
29
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
30
+ $ms = New-Object System.IO.MemoryStream
31
+ $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
32
+ $b64 = [Convert]::ToBase64String($ms.ToArray())
33
+ $ms.Dispose()
34
+ $img.Dispose()
35
+ $json = @{ type = 'image'; content = $b64 } | ConvertTo-Json -Compress
36
+ Write-Output $json
37
+ return
38
+ }
39
+
40
+ # Text
41
+ if ($data.ContainsText()) {
42
+ $text = [System.Windows.Forms.Clipboard]::GetText()
43
+ $json = @{ type = 'text'; content = $text } | ConvertTo-Json -Compress
44
+ Write-Output $json
45
+ return
46
+ }
47
+
48
+ Write-Output '{"type":"unknown","content":"Clipboard contains unsupported format"}'
49
+ }`;
50
+ const result = await runPowerShell(ps, { timeout: 10000 });
51
+ if (result.exitCode !== 0) {
52
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
53
+ }
54
+ try {
55
+ const data = JSON.parse(result.stdout);
56
+ if (data.type === 'image') {
57
+ return {
58
+ content: [
59
+ { type: 'text', text: 'Clipboard contains an image:' },
60
+ { type: 'image', data: data.content, mimeType: 'image/png' },
61
+ ],
62
+ };
63
+ }
64
+ if (data.type === 'files') {
65
+ const files = Array.isArray(data.content) ? data.content : [data.content];
66
+ return {
67
+ content: [{ type: 'text', text: `Clipboard contains ${files.length} file(s):\n${files.join('\n')}` }],
68
+ };
69
+ }
70
+ return {
71
+ content: [{ type: 'text', text: data.content || '(empty)' }],
72
+ };
73
+ }
74
+ catch {
75
+ return { content: [{ type: 'text', text: result.stdout || '(empty clipboard)' }] };
76
+ }
77
+ });
78
+ server.tool('windows_clipboard_set', 'Set clipboard contents: text, file list, or clear.', {
79
+ text: z.string().optional().describe('Text to copy to clipboard'),
80
+ files: z.array(z.string()).optional().describe('File paths to copy to clipboard'),
81
+ clear: z.boolean().optional().describe('Clear the clipboard'),
82
+ }, async ({ text, files, clear }) => {
83
+ if (clear) {
84
+ const result = await runPowerShell(`
85
+ powershell.exe -NoProfile -STA -Command {
86
+ Add-Type -AssemblyName System.Windows.Forms
87
+ [System.Windows.Forms.Clipboard]::Clear()
88
+ "Clipboard cleared"
89
+ }`);
90
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
91
+ }
92
+ if (text) {
93
+ // Escape for PowerShell here-string
94
+ const escaped = text.replace(/'/g, "''");
95
+ const result = await runPowerShell(`
96
+ powershell.exe -NoProfile -STA -Command {
97
+ Add-Type -AssemblyName System.Windows.Forms
98
+ [System.Windows.Forms.Clipboard]::SetText('${escaped}')
99
+ "Copied $([System.Windows.Forms.Clipboard]::GetText().Length) chars to clipboard"
100
+ }`);
101
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
102
+ }
103
+ if (files && files.length > 0) {
104
+ const pathList = files.map(f => `$fc.Add('${f.replace(/'/g, "''")}')`).join('; ');
105
+ const result = await runPowerShell(`
106
+ powershell.exe -NoProfile -STA -Command {
107
+ Add-Type -AssemblyName System.Windows.Forms
108
+ $fc = New-Object System.Collections.Specialized.StringCollection
109
+ ${pathList}
110
+ [System.Windows.Forms.Clipboard]::SetFileDropList($fc)
111
+ "Copied $($fc.Count) file(s) to clipboard"
112
+ }`);
113
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
114
+ }
115
+ return { content: [{ type: 'text', text: 'Provide text, files, or clear.' }], isError: true };
116
+ });
117
+ }
118
+ //# sourceMappingURL=clipboard.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerConfigTools(server: McpServer): void;
3
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,85 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_mcp_config (#40)
5
+ */
6
+ import { z } from 'zod';
7
+ import { readFile, writeFile } from 'node:fs/promises';
8
+ import { resolve } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+ const CONFIG_PATH = resolve(homedir(), '.mcp_windows.json');
11
+ const DEFAULT_CONFIG = {
12
+ blockedCommands: ['Format-Volume', 'Clear-Disk', 'Remove-Partition'],
13
+ allowedDirectories: [],
14
+ outputLineLimit: 5000,
15
+ commandTimeout: 30000,
16
+ };
17
+ let currentConfig = null;
18
+ async function loadConfig() {
19
+ if (currentConfig)
20
+ return currentConfig;
21
+ try {
22
+ const raw = await readFile(CONFIG_PATH, 'utf-8');
23
+ currentConfig = { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
24
+ }
25
+ catch {
26
+ currentConfig = { ...DEFAULT_CONFIG };
27
+ }
28
+ return currentConfig;
29
+ }
30
+ async function saveConfig(config) {
31
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
32
+ currentConfig = config;
33
+ }
34
+ export function registerConfigTools(server) {
35
+ server.tool('windows_mcp_config', 'Get or set mcp_windows configuration (blocked commands, allowed directories, output limits).', {
36
+ action: z.enum(['get', 'set']).default('get').describe('Get or set config'),
37
+ key: z.string().optional().describe('Config key to set (blockedCommands, allowedDirectories, outputLineLimit, commandTimeout)'),
38
+ value: z.string().optional().describe('Value to set (JSON for arrays, number for limits)'),
39
+ }, async ({ action, key, value }) => {
40
+ const config = await loadConfig();
41
+ if (action === 'get') {
42
+ return {
43
+ content: [{
44
+ type: 'text',
45
+ text: [
46
+ `mcp_windows configuration (${CONFIG_PATH}):`,
47
+ ``,
48
+ `Blocked commands: ${config.blockedCommands.join(', ') || '(none)'}`,
49
+ `Allowed directories: ${config.allowedDirectories.length > 0 ? config.allowedDirectories.join(', ') : '(unrestricted)'}`,
50
+ `Output line limit: ${config.outputLineLimit}`,
51
+ `Command timeout: ${config.commandTimeout}ms`,
52
+ ].join('\n'),
53
+ }],
54
+ };
55
+ }
56
+ if (!key || value === undefined) {
57
+ return { content: [{ type: 'text', text: 'Set requires key and value.' }], isError: true };
58
+ }
59
+ switch (key) {
60
+ case 'blockedCommands':
61
+ case 'allowedDirectories':
62
+ try {
63
+ config[key] = JSON.parse(value);
64
+ }
65
+ catch {
66
+ return { content: [{ type: 'text', text: `Value must be a JSON array (e.g. ["cmd1","cmd2"])` }], isError: true };
67
+ }
68
+ break;
69
+ case 'outputLineLimit':
70
+ case 'commandTimeout': {
71
+ const num = Number(value);
72
+ if (isNaN(num) || num < 0) {
73
+ return { content: [{ type: 'text', text: 'Value must be a positive number.' }], isError: true };
74
+ }
75
+ config[key] = num;
76
+ break;
77
+ }
78
+ default:
79
+ return { content: [{ type: 'text', text: `Unknown key: ${key}. Valid: blockedCommands, allowedDirectories, outputLineLimit, commandTimeout` }], isError: true };
80
+ }
81
+ await saveConfig(config);
82
+ return { content: [{ type: 'text', text: `Set ${key} = ${value}` }] };
83
+ });
84
+ }
85
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerDialogTools(server: McpServer): void;
3
+ //# sourceMappingURL=dialog.d.ts.map
@@ -0,0 +1,72 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_dialog (#21)
5
+ */
6
+ import { z } from 'zod';
7
+ import { runPowerShell } from '../shell.js';
8
+ export function registerDialogTools(server) {
9
+ server.tool('windows_dialog', 'Show system dialogs: message box, input prompt, file/folder picker.', {
10
+ type: z.enum(['message', 'input', 'file_open', 'file_save', 'folder']).describe('Dialog type'),
11
+ title: z.string().default('mcp_windows').describe('Dialog title'),
12
+ message: z.string().optional().describe('Message text (for message/input)'),
13
+ buttons: z.enum(['ok', 'okcancel', 'yesno', 'yesnocancel']).default('ok').describe('Buttons (for message)'),
14
+ filter: z.string().optional().describe('File filter (for file dialogs, e.g. "Text files|*.txt|All files|*.*")'),
15
+ default_path: z.string().optional().describe('Default path/filename'),
16
+ }, async ({ type, title, message, buttons, filter, default_path }) => {
17
+ let ps;
18
+ switch (type) {
19
+ case 'message': {
20
+ const btnMap = { ok: 0, okcancel: 1, yesno: 4, yesnocancel: 3 };
21
+ ps = `
22
+ Add-Type -AssemblyName System.Windows.Forms
23
+ $result = [System.Windows.Forms.MessageBox]::Show('${(message || '').replace(/'/g, "''")}', '${title.replace(/'/g, "''")}', ${btnMap[buttons]})
24
+ $result.ToString()`;
25
+ break;
26
+ }
27
+ case 'input':
28
+ ps = `
29
+ Add-Type -AssemblyName Microsoft.VisualBasic
30
+ $result = [Microsoft.VisualBasic.Interaction]::InputBox('${(message || 'Enter value:').replace(/'/g, "''")}', '${title.replace(/'/g, "''")}', '${(default_path || '').replace(/'/g, "''")}')
31
+ if ($result) { $result } else { '__CANCELLED__' }`;
32
+ break;
33
+ case 'file_open':
34
+ ps = `
35
+ Add-Type -AssemblyName System.Windows.Forms
36
+ $dlg = New-Object System.Windows.Forms.OpenFileDialog
37
+ $dlg.Title = '${title.replace(/'/g, "''")}'
38
+ ${filter ? `$dlg.Filter = '${filter.replace(/'/g, "''")}'` : ''}
39
+ ${default_path ? `$dlg.InitialDirectory = '${default_path.replace(/'/g, "''")}'` : ''}
40
+ $dlg.Multiselect = $false
41
+ if ($dlg.ShowDialog() -eq 'OK') { $dlg.FileName } else { '__CANCELLED__' }`;
42
+ break;
43
+ case 'file_save':
44
+ ps = `
45
+ Add-Type -AssemblyName System.Windows.Forms
46
+ $dlg = New-Object System.Windows.Forms.SaveFileDialog
47
+ $dlg.Title = '${title.replace(/'/g, "''")}'
48
+ ${filter ? `$dlg.Filter = '${filter.replace(/'/g, "''")}'` : ''}
49
+ ${default_path ? `$dlg.FileName = '${default_path.replace(/'/g, "''")}'` : ''}
50
+ if ($dlg.ShowDialog() -eq 'OK') { $dlg.FileName } else { '__CANCELLED__' }`;
51
+ break;
52
+ case 'folder':
53
+ ps = `
54
+ Add-Type -AssemblyName System.Windows.Forms
55
+ $dlg = New-Object System.Windows.Forms.FolderBrowserDialog
56
+ $dlg.Description = '${(message || title).replace(/'/g, "''")}'
57
+ ${default_path ? `$dlg.SelectedPath = '${default_path.replace(/'/g, "''")}'` : ''}
58
+ if ($dlg.ShowDialog() -eq 'OK') { $dlg.SelectedPath } else { '__CANCELLED__' }`;
59
+ break;
60
+ }
61
+ const result = await runPowerShell(ps, { timeout: 120000 }); // Long timeout — user interaction
62
+ if (result.exitCode !== 0) {
63
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
64
+ }
65
+ const output = result.stdout.trim();
66
+ if (output === '__CANCELLED__') {
67
+ return { content: [{ type: 'text', text: 'Dialog cancelled by user.' }] };
68
+ }
69
+ return { content: [{ type: 'text', text: output }] };
70
+ });
71
+ }
72
+ //# sourceMappingURL=dialog.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerDisplayTools(server: McpServer): void;
3
+ //# sourceMappingURL=display.d.ts.map