@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,138 @@
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
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ export function registerClipboardTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_clipboard_get',
14
+ 'Read clipboard contents: text, file list, or image (returned as base64).',
15
+ {},
16
+ async () => {
17
+ // PowerShell must run in STA mode for clipboard access
18
+ const ps = `
19
+ powershell.exe -NoProfile -STA -Command {
20
+ Add-Type -AssemblyName System.Windows.Forms
21
+ $data = [System.Windows.Forms.Clipboard]::GetDataObject()
22
+ if (-not $data) { Write-Output '{"type":"empty","content":"Clipboard is empty"}'; return }
23
+
24
+ # Check for files
25
+ if ($data.ContainsFileDropList()) {
26
+ $files = [System.Windows.Forms.Clipboard]::GetFileDropList()
27
+ $list = @()
28
+ foreach ($f in $files) { $list += $f }
29
+ $json = @{ type = 'files'; content = $list } | ConvertTo-Json -Compress
30
+ Write-Output $json
31
+ return
32
+ }
33
+
34
+ # Check for image
35
+ if ($data.ContainsImage()) {
36
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
37
+ $ms = New-Object System.IO.MemoryStream
38
+ $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
39
+ $b64 = [Convert]::ToBase64String($ms.ToArray())
40
+ $ms.Dispose()
41
+ $img.Dispose()
42
+ $json = @{ type = 'image'; content = $b64 } | ConvertTo-Json -Compress
43
+ Write-Output $json
44
+ return
45
+ }
46
+
47
+ # Text
48
+ if ($data.ContainsText()) {
49
+ $text = [System.Windows.Forms.Clipboard]::GetText()
50
+ $json = @{ type = 'text'; content = $text } | ConvertTo-Json -Compress
51
+ Write-Output $json
52
+ return
53
+ }
54
+
55
+ Write-Output '{"type":"unknown","content":"Clipboard contains unsupported format"}'
56
+ }`;
57
+
58
+ const result = await runPowerShell(ps, { timeout: 10000 });
59
+ if (result.exitCode !== 0) {
60
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
61
+ }
62
+
63
+ try {
64
+ const data = JSON.parse(result.stdout);
65
+
66
+ if (data.type === 'image') {
67
+ return {
68
+ content: [
69
+ { type: 'text' as const, text: 'Clipboard contains an image:' },
70
+ { type: 'image' as const, data: data.content, mimeType: 'image/png' },
71
+ ],
72
+ };
73
+ }
74
+
75
+ if (data.type === 'files') {
76
+ const files = Array.isArray(data.content) ? data.content : [data.content];
77
+ return {
78
+ content: [{ type: 'text', text: `Clipboard contains ${files.length} file(s):\n${files.join('\n')}` }],
79
+ };
80
+ }
81
+
82
+ return {
83
+ content: [{ type: 'text', text: data.content || '(empty)' }],
84
+ };
85
+ } catch {
86
+ return { content: [{ type: 'text', text: result.stdout || '(empty clipboard)' }] };
87
+ }
88
+ },
89
+ );
90
+
91
+ server.tool(
92
+ 'windows_clipboard_set',
93
+ 'Set clipboard contents: text, file list, or clear.',
94
+ {
95
+ text: z.string().optional().describe('Text to copy to clipboard'),
96
+ files: z.array(z.string()).optional().describe('File paths to copy to clipboard'),
97
+ clear: z.boolean().optional().describe('Clear the clipboard'),
98
+ },
99
+ async ({ text, files, clear }) => {
100
+ if (clear) {
101
+ const result = await runPowerShell(`
102
+ powershell.exe -NoProfile -STA -Command {
103
+ Add-Type -AssemblyName System.Windows.Forms
104
+ [System.Windows.Forms.Clipboard]::Clear()
105
+ "Clipboard cleared"
106
+ }`);
107
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
108
+ }
109
+
110
+ if (text) {
111
+ // Escape for PowerShell here-string
112
+ const escaped = text.replace(/'/g, "''");
113
+ const result = await runPowerShell(`
114
+ powershell.exe -NoProfile -STA -Command {
115
+ Add-Type -AssemblyName System.Windows.Forms
116
+ [System.Windows.Forms.Clipboard]::SetText('${escaped}')
117
+ "Copied $([System.Windows.Forms.Clipboard]::GetText().Length) chars to clipboard"
118
+ }`);
119
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
120
+ }
121
+
122
+ if (files && files.length > 0) {
123
+ const pathList = files.map(f => `$fc.Add('${f.replace(/'/g, "''")}')`).join('; ');
124
+ const result = await runPowerShell(`
125
+ powershell.exe -NoProfile -STA -Command {
126
+ Add-Type -AssemblyName System.Windows.Forms
127
+ $fc = New-Object System.Collections.Specialized.StringCollection
128
+ ${pathList}
129
+ [System.Windows.Forms.Clipboard]::SetFileDropList($fc)
130
+ "Copied $($fc.Count) file(s) to clipboard"
131
+ }`);
132
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
133
+ }
134
+
135
+ return { content: [{ type: 'text', text: 'Provide text, files, or clear.' }], isError: true };
136
+ },
137
+ );
138
+ }
@@ -0,0 +1,105 @@
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
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { readFile, writeFile } from 'node:fs/promises';
10
+ import { resolve } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+
13
+ const CONFIG_PATH = resolve(homedir(), '.mcp_windows.json');
14
+
15
+ interface McpConfig {
16
+ blockedCommands: string[];
17
+ allowedDirectories: string[];
18
+ outputLineLimit: number;
19
+ commandTimeout: number;
20
+ }
21
+
22
+ const DEFAULT_CONFIG: McpConfig = {
23
+ blockedCommands: ['Format-Volume', 'Clear-Disk', 'Remove-Partition'],
24
+ allowedDirectories: [],
25
+ outputLineLimit: 5000,
26
+ commandTimeout: 30000,
27
+ };
28
+
29
+ let currentConfig: McpConfig | null = null;
30
+
31
+ async function loadConfig(): Promise<McpConfig> {
32
+ if (currentConfig) return currentConfig;
33
+ try {
34
+ const raw = await readFile(CONFIG_PATH, 'utf-8');
35
+ currentConfig = { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
36
+ } catch {
37
+ currentConfig = { ...DEFAULT_CONFIG };
38
+ }
39
+ return currentConfig!;
40
+ }
41
+
42
+ async function saveConfig(config: McpConfig): Promise<void> {
43
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
44
+ currentConfig = config;
45
+ }
46
+
47
+ export function registerConfigTools(server: McpServer): void {
48
+ server.tool(
49
+ 'windows_mcp_config',
50
+ 'Get or set mcp_windows configuration (blocked commands, allowed directories, output limits).',
51
+ {
52
+ action: z.enum(['get', 'set']).default('get').describe('Get or set config'),
53
+ key: z.string().optional().describe('Config key to set (blockedCommands, allowedDirectories, outputLineLimit, commandTimeout)'),
54
+ value: z.string().optional().describe('Value to set (JSON for arrays, number for limits)'),
55
+ },
56
+ async ({ action, key, value }) => {
57
+ const config = await loadConfig();
58
+
59
+ if (action === 'get') {
60
+ return {
61
+ content: [{
62
+ type: 'text',
63
+ text: [
64
+ `mcp_windows configuration (${CONFIG_PATH}):`,
65
+ ``,
66
+ `Blocked commands: ${config.blockedCommands.join(', ') || '(none)'}`,
67
+ `Allowed directories: ${config.allowedDirectories.length > 0 ? config.allowedDirectories.join(', ') : '(unrestricted)'}`,
68
+ `Output line limit: ${config.outputLineLimit}`,
69
+ `Command timeout: ${config.commandTimeout}ms`,
70
+ ].join('\n'),
71
+ }],
72
+ };
73
+ }
74
+
75
+ if (!key || value === undefined) {
76
+ return { content: [{ type: 'text', text: 'Set requires key and value.' }], isError: true };
77
+ }
78
+
79
+ switch (key) {
80
+ case 'blockedCommands':
81
+ case 'allowedDirectories':
82
+ try {
83
+ (config as unknown as Record<string, unknown>)[key] = JSON.parse(value);
84
+ } catch {
85
+ return { content: [{ type: 'text', text: `Value must be a JSON array (e.g. ["cmd1","cmd2"])` }], isError: true };
86
+ }
87
+ break;
88
+ case 'outputLineLimit':
89
+ case 'commandTimeout': {
90
+ const num = Number(value);
91
+ if (isNaN(num) || num < 0) {
92
+ return { content: [{ type: 'text', text: 'Value must be a positive number.' }], isError: true };
93
+ }
94
+ (config as unknown as Record<string, unknown>)[key] = num;
95
+ break;
96
+ }
97
+ default:
98
+ return { content: [{ type: 'text', text: `Unknown key: ${key}. Valid: blockedCommands, allowedDirectories, outputLineLimit, commandTimeout` }], isError: true };
99
+ }
100
+
101
+ await saveConfig(config);
102
+ return { content: [{ type: 'text', text: `Set ${key} = ${value}` }] };
103
+ },
104
+ );
105
+ }
@@ -0,0 +1,87 @@
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
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ export function registerDialogTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_dialog',
14
+ 'Show system dialogs: message box, input prompt, file/folder picker.',
15
+ {
16
+ type: z.enum(['message', 'input', 'file_open', 'file_save', 'folder']).describe('Dialog type'),
17
+ title: z.string().default('mcp_windows').describe('Dialog title'),
18
+ message: z.string().optional().describe('Message text (for message/input)'),
19
+ buttons: z.enum(['ok', 'okcancel', 'yesno', 'yesnocancel']).default('ok').describe('Buttons (for message)'),
20
+ filter: z.string().optional().describe('File filter (for file dialogs, e.g. "Text files|*.txt|All files|*.*")'),
21
+ default_path: z.string().optional().describe('Default path/filename'),
22
+ },
23
+ async ({ type, title, message, buttons, filter, default_path }) => {
24
+ let ps: string;
25
+
26
+ switch (type) {
27
+ case 'message': {
28
+ const btnMap: Record<string, number> = { ok: 0, okcancel: 1, yesno: 4, yesnocancel: 3 };
29
+ ps = `
30
+ Add-Type -AssemblyName System.Windows.Forms
31
+ $result = [System.Windows.Forms.MessageBox]::Show('${(message || '').replace(/'/g, "''")}', '${title.replace(/'/g, "''")}', ${btnMap[buttons]})
32
+ $result.ToString()`;
33
+ break;
34
+ }
35
+
36
+ case 'input':
37
+ ps = `
38
+ Add-Type -AssemblyName Microsoft.VisualBasic
39
+ $result = [Microsoft.VisualBasic.Interaction]::InputBox('${(message || 'Enter value:').replace(/'/g, "''")}', '${title.replace(/'/g, "''")}', '${(default_path || '').replace(/'/g, "''")}')
40
+ if ($result) { $result } else { '__CANCELLED__' }`;
41
+ break;
42
+
43
+ case 'file_open':
44
+ ps = `
45
+ Add-Type -AssemblyName System.Windows.Forms
46
+ $dlg = New-Object System.Windows.Forms.OpenFileDialog
47
+ $dlg.Title = '${title.replace(/'/g, "''")}'
48
+ ${filter ? `$dlg.Filter = '${filter.replace(/'/g, "''")}'` : ''}
49
+ ${default_path ? `$dlg.InitialDirectory = '${default_path.replace(/'/g, "''")}'` : ''}
50
+ $dlg.Multiselect = $false
51
+ if ($dlg.ShowDialog() -eq 'OK') { $dlg.FileName } else { '__CANCELLED__' }`;
52
+ break;
53
+
54
+ case 'file_save':
55
+ ps = `
56
+ Add-Type -AssemblyName System.Windows.Forms
57
+ $dlg = New-Object System.Windows.Forms.SaveFileDialog
58
+ $dlg.Title = '${title.replace(/'/g, "''")}'
59
+ ${filter ? `$dlg.Filter = '${filter.replace(/'/g, "''")}'` : ''}
60
+ ${default_path ? `$dlg.FileName = '${default_path.replace(/'/g, "''")}'` : ''}
61
+ if ($dlg.ShowDialog() -eq 'OK') { $dlg.FileName } else { '__CANCELLED__' }`;
62
+ break;
63
+
64
+ case 'folder':
65
+ ps = `
66
+ Add-Type -AssemblyName System.Windows.Forms
67
+ $dlg = New-Object System.Windows.Forms.FolderBrowserDialog
68
+ $dlg.Description = '${(message || title).replace(/'/g, "''")}'
69
+ ${default_path ? `$dlg.SelectedPath = '${default_path.replace(/'/g, "''")}'` : ''}
70
+ if ($dlg.ShowDialog() -eq 'OK') { $dlg.SelectedPath } else { '__CANCELLED__' }`;
71
+ break;
72
+ }
73
+
74
+ const result = await runPowerShell(ps, { timeout: 120000 }); // Long timeout — user interaction
75
+ if (result.exitCode !== 0) {
76
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
77
+ }
78
+
79
+ const output = result.stdout.trim();
80
+ if (output === '__CANCELLED__') {
81
+ return { content: [{ type: 'text', text: 'Dialog cancelled by user.' }] };
82
+ }
83
+
84
+ return { content: [{ type: 'text', text: output }] };
85
+ },
86
+ );
87
+ }
@@ -0,0 +1,285 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_display_get (#9), windows_display_set (#10), windows_screenshot (#11)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+ import { readFile } from 'node:fs/promises';
11
+ import { resolve } from 'node:path';
12
+ import { tmpdir } from 'node:os';
13
+
14
+ export function registerDisplayTools(server: McpServer): void {
15
+ server.tool(
16
+ 'windows_display_get',
17
+ 'Get display configuration: resolution, refresh rate, scaling, multi-monitor layout, HDR status.',
18
+ {},
19
+ async () => {
20
+ const ps = `
21
+ Add-Type -AssemblyName System.Windows.Forms
22
+
23
+ $screens = [System.Windows.Forms.Screen]::AllScreens
24
+ $monitors = Get-CimInstance -Namespace root\\wmi -ClassName WmiMonitorBasicDisplayParams -ErrorAction SilentlyContinue
25
+ $videoCtrl = Get-CimInstance Win32_VideoController
26
+
27
+ $i = 0
28
+ $screens | ForEach-Object {
29
+ $s = $_
30
+ $vc = $videoCtrl | Where-Object { $_.Name -match $s.DeviceName -or $true } | Select-Object -First 1
31
+ $dpiScale = [math]::Round(($s.Bounds.Width / $s.WorkingArea.Width) * 100, 0)
32
+
33
+ # Try to get real scaling from registry
34
+ $regScale = $null
35
+ try {
36
+ $regPath = "HKCU:\\Control Panel\\Desktop\\PerMonitorSettings"
37
+ if (Test-Path $regPath) {
38
+ $regScale = (Get-ChildItem $regPath -ErrorAction SilentlyContinue | Select-Object -Index $i | Get-ItemProperty -ErrorAction SilentlyContinue).DpiValue
39
+ }
40
+ } catch {}
41
+
42
+ [PSCustomObject]@{
43
+ Index = $i
44
+ Name = $s.DeviceName
45
+ Primary = $s.Primary
46
+ Resolution = "$($s.Bounds.Width)x$($s.Bounds.Height)"
47
+ RefreshRate = if ($vc) { "$($vc.CurrentRefreshRate) Hz" } else { 'Unknown' }
48
+ Scaling = if ($regScale) { "$($regScale)%" } else { 'System default' }
49
+ Position = "($($s.Bounds.X), $($s.Bounds.Y))"
50
+ WorkArea = "$($s.WorkingArea.Width)x$($s.WorkingArea.Height)"
51
+ BitsPerPixel = if ($vc) { $vc.CurrentBitsPerPixel } else { $null }
52
+ }
53
+ $i++
54
+ } | ConvertTo-Json -Depth 3 -Compress`;
55
+
56
+ const result = await runPowerShell(ps, { timeout: 10000 });
57
+ if (result.exitCode !== 0) {
58
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
59
+ }
60
+
61
+ const displays = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
62
+ const lines = displays.map((d: { Index: number; Name: string; Primary: boolean; Resolution: string; RefreshRate: string; Scaling: string; Position: string; WorkArea: string }) =>
63
+ [
64
+ `Monitor ${d.Index}: ${d.Name}${d.Primary ? ' (Primary)' : ''}`,
65
+ ` Resolution: ${d.Resolution} @ ${d.RefreshRate}`,
66
+ ` Scaling: ${d.Scaling}`,
67
+ ` Position: ${d.Position}`,
68
+ ` Work area: ${d.WorkArea}`,
69
+ ].join('\n'),
70
+ );
71
+
72
+ return { content: [{ type: 'text', text: lines.join('\n\n') }] };
73
+ },
74
+ );
75
+
76
+ server.tool(
77
+ 'windows_display_set',
78
+ 'Change display settings: resolution, brightness.',
79
+ {
80
+ resolution: z.string().optional().describe('Resolution as "WIDTHxHEIGHT" (e.g. "1920x1080")'),
81
+ brightness: z.number().min(0).max(100).optional().describe('Screen brightness 0-100 (laptops only)'),
82
+ },
83
+ async ({ resolution, brightness }) => {
84
+ const results: string[] = [];
85
+
86
+ if (brightness !== undefined) {
87
+ const ps = `
88
+ try {
89
+ $monitors = Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightnessMethods -ErrorAction Stop
90
+ $monitors | Invoke-CimMethod -MethodName WmiSetBrightness -Arguments @{Timeout=1; Brightness=${brightness}} -ErrorAction Stop
91
+ "Brightness set to ${brightness}%"
92
+ } catch {
93
+ "Error: Brightness control not available (requires laptop/integrated display). $_"
94
+ }`;
95
+ const r = await runPowerShell(ps);
96
+ results.push(r.stdout || r.stderr);
97
+ }
98
+
99
+ if (resolution) {
100
+ const match = resolution.match(/^(\d+)x(\d+)$/);
101
+ if (!match) {
102
+ results.push('Invalid resolution format. Use WIDTHxHEIGHT (e.g. 1920x1080)');
103
+ } else {
104
+ const ps = `
105
+ Add-Type @'
106
+ using System;
107
+ using System.Runtime.InteropServices;
108
+
109
+ public class DisplaySettings {
110
+ [DllImport("user32.dll")]
111
+ public static extern int EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
112
+ [DllImport("user32.dll")]
113
+ public static extern int ChangeDisplaySettings(ref DEVMODE devMode, int flags);
114
+
115
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
116
+ public struct DEVMODE {
117
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
118
+ public string dmDeviceName;
119
+ public short dmSpecVersion;
120
+ public short dmDriverVersion;
121
+ public short dmSize;
122
+ public short dmDriverExtra;
123
+ public int dmFields;
124
+ public int dmPositionX;
125
+ public int dmPositionY;
126
+ public int dmDisplayOrientation;
127
+ public int dmDisplayFixedOutput;
128
+ public short dmColor;
129
+ public short dmDuplex;
130
+ public short dmYResolution;
131
+ public short dmTTOption;
132
+ public short dmCollate;
133
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
134
+ public string dmFormName;
135
+ public short dmLogPixels;
136
+ public int dmBitsPerPel;
137
+ public int dmPelsWidth;
138
+ public int dmPelsHeight;
139
+ public int dmDisplayFlags;
140
+ public int dmDisplayFrequency;
141
+ }
142
+
143
+ public static string SetResolution(int width, int height) {
144
+ DEVMODE dm = new DEVMODE();
145
+ dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));
146
+ if (EnumDisplaySettings(null, -1, ref dm) != 0) {
147
+ dm.dmPelsWidth = width;
148
+ dm.dmPelsHeight = height;
149
+ dm.dmFields = 0x80000 | 0x100000; // DM_PELSWIDTH | DM_PELSHEIGHT
150
+ int result = ChangeDisplaySettings(ref dm, 0);
151
+ if (result == 0) return "Resolution changed to " + width + "x" + height;
152
+ return "Failed to change resolution (code: " + result + "). Resolution may not be supported.";
153
+ }
154
+ return "Failed to enumerate display settings.";
155
+ }
156
+ }
157
+ '@ -ErrorAction Stop
158
+ [DisplaySettings]::SetResolution(${match[1]}, ${match[2]})`;
159
+ const r = await runPowerShell(ps);
160
+ results.push(r.stdout || r.stderr);
161
+ }
162
+ }
163
+
164
+ if (results.length === 0) {
165
+ return { content: [{ type: 'text', text: 'No changes specified. Provide resolution or brightness.' }], isError: true };
166
+ }
167
+
168
+ return { content: [{ type: 'text', text: results.join('\n') }] };
169
+ },
170
+ );
171
+
172
+ server.tool(
173
+ 'windows_screenshot',
174
+ 'Capture a screenshot of the screen, a specific window, or a region. Returns as base64 image.',
175
+ {
176
+ target: z.enum(['screen', 'window', 'region']).default('screen').describe('What to capture'),
177
+ monitor: z.number().default(0).describe('Monitor index (for screen capture)'),
178
+ window_title: z.string().optional().describe('Window title substring (for window capture)'),
179
+ x: z.number().optional().describe('Region X (for region capture)'),
180
+ y: z.number().optional().describe('Region Y'),
181
+ width: z.number().optional().describe('Region width'),
182
+ height: z.number().optional().describe('Region height'),
183
+ save_path: z.string().optional().describe('Save to file instead of returning base64'),
184
+ },
185
+ async ({ target, monitor, window_title, x, y, width, height, save_path }) => {
186
+ const outPath = save_path ? resolve(save_path) : resolve(tmpdir(), `screenshot_${Date.now()}.png`);
187
+
188
+ let ps: string;
189
+
190
+ if (target === 'window' && window_title) {
191
+ ps = `
192
+ Add-Type -AssemblyName System.Windows.Forms
193
+ Add-Type -AssemblyName System.Drawing
194
+ Add-Type @'
195
+ using System;
196
+ using System.Runtime.InteropServices;
197
+ public class Win32Window {
198
+ [DllImport("user32.dll")] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
199
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
200
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
201
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
202
+ [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);
203
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
204
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
205
+ [StructLayout(LayoutKind.Sequential)]
206
+ public struct RECT { public int Left, Top, Right, Bottom; }
207
+ }
208
+ '@
209
+ $target = '${window_title.replace(/'/g, "''")}'
210
+ $found = $null
211
+ [Win32Window]::EnumWindows({
212
+ param($hWnd, $lParam)
213
+ $sb = New-Object System.Text.StringBuilder 256
214
+ [Win32Window]::GetWindowText($hWnd, $sb, 256) | Out-Null
215
+ $title = $sb.ToString()
216
+ if ($title -like "*$target*") { $script:found = $hWnd; return $false }
217
+ return $true
218
+ }, [IntPtr]::Zero) | Out-Null
219
+
220
+ if (-not $found) { throw "Window not found: $target" }
221
+
222
+ $rect = New-Object Win32Window+RECT
223
+ [Win32Window]::GetWindowRect($found, [ref]$rect) | Out-Null
224
+ $w = $rect.Right - $rect.Left
225
+ $h = $rect.Bottom - $rect.Top
226
+ $bmp = New-Object System.Drawing.Bitmap($w, $h)
227
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
228
+ $g.CopyFromScreen($rect.Left, $rect.Top, 0, 0, [System.Drawing.Size]::new($w, $h))
229
+ $g.Dispose()
230
+ $bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
231
+ $bmp.Dispose()
232
+ "saved"`;
233
+ } else if (target === 'region' && x !== undefined && y !== undefined && width && height) {
234
+ ps = `
235
+ Add-Type -AssemblyName System.Drawing
236
+ $bmp = New-Object System.Drawing.Bitmap(${width}, ${height})
237
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
238
+ $g.CopyFromScreen(${x}, ${y}, 0, 0, [System.Drawing.Size]::new(${width}, ${height}))
239
+ $g.Dispose()
240
+ $bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
241
+ $bmp.Dispose()
242
+ "saved"`;
243
+ } else {
244
+ // Full screen
245
+ ps = `
246
+ Add-Type -AssemblyName System.Windows.Forms
247
+ Add-Type -AssemblyName System.Drawing
248
+ $screen = [System.Windows.Forms.Screen]::AllScreens[${monitor}]
249
+ $bounds = $screen.Bounds
250
+ $bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
251
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
252
+ $g.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size)
253
+ $g.Dispose()
254
+ $bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
255
+ $bmp.Dispose()
256
+ "saved"`;
257
+ }
258
+
259
+ const result = await runPowerShell(ps, { timeout: 10000 });
260
+ if (result.exitCode !== 0) {
261
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
262
+ }
263
+
264
+ if (save_path) {
265
+ return { content: [{ type: 'text', text: `Screenshot saved: ${outPath}` }] };
266
+ }
267
+
268
+ // Return as base64 image
269
+ try {
270
+ const data = await readFile(outPath);
271
+ // Clean up temp file
272
+ await import('node:fs/promises').then(fs => fs.unlink(outPath)).catch(() => {});
273
+ return {
274
+ content: [{
275
+ type: 'image' as const,
276
+ data: data.toString('base64'),
277
+ mimeType: 'image/png',
278
+ }],
279
+ };
280
+ } catch {
281
+ return { content: [{ type: 'text', text: `Screenshot captured at ${outPath} but failed to read back.` }] };
282
+ }
283
+ },
284
+ );
285
+ }