@mokoconsulting/mcp-windows 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.gitattributes +94 -0
  2. package/.gitmessage +9 -0
  3. package/.mokogitea/ISSUE_TEMPLATE/adr.md +110 -0
  4. package/.mokogitea/ISSUE_TEMPLATE/bug_report.md +48 -0
  5. package/.mokogitea/ISSUE_TEMPLATE/config.yml +18 -0
  6. package/.mokogitea/ISSUE_TEMPLATE/documentation.md +52 -0
  7. package/.mokogitea/ISSUE_TEMPLATE/feature_request.md +51 -0
  8. package/.mokogitea/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
  9. package/.mokogitea/ISSUE_TEMPLATE/mcp_connection_issue.md +67 -0
  10. package/.mokogitea/ISSUE_TEMPLATE/mcp_tool_request.md +49 -0
  11. package/.mokogitea/ISSUE_TEMPLATE/question.md +82 -0
  12. package/.mokogitea/ISSUE_TEMPLATE/rfc.md +126 -0
  13. package/.mokogitea/ISSUE_TEMPLATE/security.md +51 -0
  14. package/.mokogitea/ISSUE_TEMPLATE/version.md +24 -0
  15. package/.mokogitea/branch-protection.yml +251 -0
  16. package/.mokogitea/workflows/auto-assign.yml +76 -0
  17. package/.mokogitea/workflows/auto-bump.yml +66 -0
  18. package/.mokogitea/workflows/auto-dev-issue.yml +207 -0
  19. package/.mokogitea/workflows/auto-release.yml +421 -0
  20. package/.mokogitea/workflows/branch-cleanup.yml +48 -0
  21. package/.mokogitea/workflows/cascade-dev.yml +10 -0
  22. package/.mokogitea/workflows/changelog-validation.yml +101 -0
  23. package/.mokogitea/workflows/ci-generic.yml +191 -0
  24. package/.mokogitea/workflows/cleanup.yml +87 -0
  25. package/.mokogitea/workflows/codeql-analysis.yml +115 -0
  26. package/.mokogitea/workflows/copilot-agent.yml +44 -0
  27. package/.mokogitea/workflows/deploy-manual.yml +126 -0
  28. package/.mokogitea/workflows/enterprise-firewall-setup.yml +758 -0
  29. package/.mokogitea/workflows/gitleaks.yml +92 -0
  30. package/.mokogitea/workflows/issue-branch.yml +73 -0
  31. package/.mokogitea/workflows/mcp-auto-release.yml +278 -0
  32. package/.mokogitea/workflows/mcp-build-test.yml +65 -0
  33. package/.mokogitea/workflows/mcp-sdk-check.yml +109 -0
  34. package/.mokogitea/workflows/mcp-tool-inventory.yml +61 -0
  35. package/.mokogitea/workflows/notify.yml +70 -0
  36. package/.mokogitea/workflows/npm-publish.yml +113 -0
  37. package/.mokogitea/workflows/pr-check.yml +534 -0
  38. package/.mokogitea/workflows/pre-release.yml +252 -0
  39. package/.mokogitea/workflows/rc-revert.yml +66 -0
  40. package/.mokogitea/workflows/repo-health.yml +712 -0
  41. package/.mokogitea/workflows/repository-cleanup.yml +525 -0
  42. package/.mokogitea/workflows/security-audit.yml +82 -0
  43. package/.mokogitea/workflows/standards-compliance.yml +2614 -0
  44. package/.mokogitea/workflows/sync-version-on-merge.yml +133 -0
  45. package/.mokogitea/workflows/update-server.yml +312 -0
  46. package/.mokogitea/workflows/workflow-sync-trigger.yml +73 -0
  47. package/CHANGELOG.md +130 -0
  48. package/CLAUDE.md +49 -0
  49. package/CONTRIBUTING.md +161 -0
  50. package/ISSUES.md +601 -0
  51. package/Makefile +70 -0
  52. package/README.md +80 -0
  53. package/automation/ci-issue-reporter.sh +237 -0
  54. package/config.example.json +18 -0
  55. package/dist/index.d.ts +3 -0
  56. package/dist/index.js +111 -0
  57. package/dist/shell.d.ts +50 -0
  58. package/dist/shell.js +209 -0
  59. package/dist/tools/apps.d.ts +3 -0
  60. package/dist/tools/apps.js +63 -0
  61. package/dist/tools/audio.d.ts +3 -0
  62. package/dist/tools/audio.js +142 -0
  63. package/dist/tools/audio_apps.d.ts +3 -0
  64. package/dist/tools/audio_apps.js +86 -0
  65. package/dist/tools/automation.d.ts +3 -0
  66. package/dist/tools/automation.js +261 -0
  67. package/dist/tools/bluetooth.d.ts +3 -0
  68. package/dist/tools/bluetooth.js +96 -0
  69. package/dist/tools/clipboard.d.ts +3 -0
  70. package/dist/tools/clipboard.js +118 -0
  71. package/dist/tools/config.d.ts +3 -0
  72. package/dist/tools/config.js +85 -0
  73. package/dist/tools/dialog.d.ts +3 -0
  74. package/dist/tools/dialog.js +72 -0
  75. package/dist/tools/display.d.ts +3 -0
  76. package/dist/tools/display.js +256 -0
  77. package/dist/tools/drives.d.ts +3 -0
  78. package/dist/tools/drives.js +98 -0
  79. package/dist/tools/environment.d.ts +3 -0
  80. package/dist/tools/environment.js +129 -0
  81. package/dist/tools/execute.d.ts +3 -0
  82. package/dist/tools/execute.js +28 -0
  83. package/dist/tools/filesystem.d.ts +3 -0
  84. package/dist/tools/filesystem.js +230 -0
  85. package/dist/tools/firewall.d.ts +3 -0
  86. package/dist/tools/firewall.js +108 -0
  87. package/dist/tools/hosts.d.ts +3 -0
  88. package/dist/tools/hosts.js +119 -0
  89. package/dist/tools/maintenance.d.ts +3 -0
  90. package/dist/tools/maintenance.js +236 -0
  91. package/dist/tools/netstat.d.ts +3 -0
  92. package/dist/tools/netstat.js +56 -0
  93. package/dist/tools/network.d.ts +3 -0
  94. package/dist/tools/network.js +70 -0
  95. package/dist/tools/notification.d.ts +3 -0
  96. package/dist/tools/notification.js +41 -0
  97. package/dist/tools/power.d.ts +3 -0
  98. package/dist/tools/power.js +104 -0
  99. package/dist/tools/printer.d.ts +3 -0
  100. package/dist/tools/printer.js +97 -0
  101. package/dist/tools/process.d.ts +3 -0
  102. package/dist/tools/process.js +54 -0
  103. package/dist/tools/process_kill.d.ts +3 -0
  104. package/dist/tools/process_kill.js +48 -0
  105. package/dist/tools/recycle_bin.d.ts +3 -0
  106. package/dist/tools/recycle_bin.js +108 -0
  107. package/dist/tools/registry.d.ts +3 -0
  108. package/dist/tools/registry.js +136 -0
  109. package/dist/tools/scheduler.d.ts +3 -0
  110. package/dist/tools/scheduler.js +116 -0
  111. package/dist/tools/service.d.ts +3 -0
  112. package/dist/tools/service.js +79 -0
  113. package/dist/tools/startup.d.ts +3 -0
  114. package/dist/tools/startup.js +159 -0
  115. package/dist/tools/storage.d.ts +3 -0
  116. package/dist/tools/storage.js +129 -0
  117. package/dist/tools/system.d.ts +3 -0
  118. package/dist/tools/system.js +84 -0
  119. package/dist/tools/system_mgmt.d.ts +3 -0
  120. package/dist/tools/system_mgmt.js +174 -0
  121. package/dist/tools/terminal.d.ts +3 -0
  122. package/dist/tools/terminal.js +80 -0
  123. package/dist/tools/theme.d.ts +3 -0
  124. package/dist/tools/theme.js +165 -0
  125. package/dist/tools/usb.d.ts +3 -0
  126. package/dist/tools/usb.js +52 -0
  127. package/dist/tools/virtual_desktop.d.ts +3 -0
  128. package/dist/tools/virtual_desktop.js +112 -0
  129. package/dist/tools/wifi.d.ts +3 -0
  130. package/dist/tools/wifi.js +136 -0
  131. package/dist/tools/window.d.ts +3 -0
  132. package/dist/tools/window.js +189 -0
  133. package/dist/tools/winget.d.ts +3 -0
  134. package/dist/tools/winget.js +79 -0
  135. package/dist/tools/wsl.d.ts +3 -0
  136. package/dist/tools/wsl.js +99 -0
  137. package/docs/API.md +63 -0
  138. package/docs/ARCHITECTURE.md +73 -0
  139. package/docs/INSTALLATION.md +102 -0
  140. package/docs/index.md +12 -0
  141. package/package.json +35 -0
  142. package/scripts/setup.mjs +123 -0
  143. package/src/index.ts +125 -0
  144. package/src/shell.ts +253 -0
  145. package/src/tools/apps.ts +76 -0
  146. package/src/tools/audio.ts +161 -0
  147. package/src/tools/audio_apps.ts +98 -0
  148. package/src/tools/automation.ts +297 -0
  149. package/src/tools/bluetooth.ts +114 -0
  150. package/src/tools/clipboard.ts +138 -0
  151. package/src/tools/config.ts +105 -0
  152. package/src/tools/dialog.ts +87 -0
  153. package/src/tools/display.ts +285 -0
  154. package/src/tools/drives.ts +124 -0
  155. package/src/tools/environment.ts +146 -0
  156. package/src/tools/execute.ts +35 -0
  157. package/src/tools/filesystem.ts +273 -0
  158. package/src/tools/firewall.ts +125 -0
  159. package/src/tools/hosts.ts +135 -0
  160. package/src/tools/maintenance.ts +299 -0
  161. package/src/tools/netstat.ts +72 -0
  162. package/src/tools/network.ts +84 -0
  163. package/src/tools/notification.ts +50 -0
  164. package/src/tools/power.ts +123 -0
  165. package/src/tools/printer.ts +114 -0
  166. package/src/tools/process.ts +80 -0
  167. package/src/tools/process_kill.ts +57 -0
  168. package/src/tools/recycle_bin.ts +126 -0
  169. package/src/tools/registry.ts +165 -0
  170. package/src/tools/scheduler.ts +140 -0
  171. package/src/tools/service.ts +102 -0
  172. package/src/tools/startup.ts +180 -0
  173. package/src/tools/storage.ts +141 -0
  174. package/src/tools/system.ts +99 -0
  175. package/src/tools/system_mgmt.ts +190 -0
  176. package/src/tools/terminal.ts +117 -0
  177. package/src/tools/theme.ts +205 -0
  178. package/src/tools/usb.ts +65 -0
  179. package/src/tools/virtual_desktop.ts +122 -0
  180. package/src/tools/wifi.ts +157 -0
  181. package/src/tools/window.ts +211 -0
  182. package/src/tools/winget.ts +100 -0
  183. package/src/tools/wsl.ts +112 -0
  184. package/tsconfig.json +19 -0
@@ -0,0 +1,165 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_registry_read (#29), windows_registry_write (#30)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import { runPowerShell } from '../shell.js';
10
+
11
+ function expandHive(path: string): string {
12
+ return path
13
+ .replace(/^HKLM[:\\\/]/i, 'HKLM:\\')
14
+ .replace(/^HKCU[:\\\/]/i, 'HKCU:\\')
15
+ .replace(/^HKCR[:\\\/]/i, 'HKCR:\\')
16
+ .replace(/^HKU[:\\\/]/i, 'HKU:\\')
17
+ .replace(/^HKCC[:\\\/]/i, 'HKCC:\\');
18
+ }
19
+
20
+ export function registerRegistryTools(server: McpServer): void {
21
+ server.tool(
22
+ 'windows_registry_read',
23
+ 'Read Windows Registry keys, subkeys, and values. Supports HKLM, HKCU, HKCR abbreviations.',
24
+ {
25
+ path: z.string().describe('Registry path (e.g. "HKCU:\\Software\\Microsoft")'),
26
+ value: z.string().optional().describe('Specific value name to read (omit to list all values)'),
27
+ subkeys: z.boolean().default(false).describe('List subkeys instead of values'),
28
+ },
29
+ async ({ path, value, subkeys }) => {
30
+ const regPath = expandHive(path);
31
+
32
+ if (subkeys) {
33
+ const ps = `
34
+ Get-ChildItem -Path '${regPath.replace(/'/g, "''")}' -ErrorAction Stop | ForEach-Object {
35
+ [PSCustomObject]@{
36
+ Name = $_.PSChildName
37
+ SubKeyCount = $_.SubKeyCount
38
+ ValueCount = $_.ValueCount
39
+ }
40
+ } | ConvertTo-Json -Depth 3 -Compress`;
41
+
42
+ const result = await runPowerShell(ps, { timeout: 10000 });
43
+ if (result.exitCode !== 0) {
44
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
45
+ }
46
+ if (!result.stdout) {
47
+ return { content: [{ type: 'text', text: 'No subkeys found.' }] };
48
+ }
49
+
50
+ const keys = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
51
+ const lines = keys.map((k: { Name: string; SubKeyCount: number; ValueCount: number }) =>
52
+ ` ${k.Name.padEnd(40)} ${k.SubKeyCount} subkeys, ${k.ValueCount} values`,
53
+ );
54
+ return { content: [{ type: 'text', text: `${regPath}\n${'─'.repeat(70)}\n${lines.join('\n')}` }] };
55
+ }
56
+
57
+ if (value) {
58
+ const ps = `
59
+ $v = Get-ItemProperty -Path '${regPath.replace(/'/g, "''")}' -Name '${value.replace(/'/g, "''")}' -ErrorAction Stop
60
+ $raw = $v.'${value.replace(/'/g, "''")}'
61
+ $kind = (Get-Item -Path '${regPath.replace(/'/g, "''")}' -ErrorAction Stop).GetValueKind('${value.replace(/'/g, "''")}')
62
+ [PSCustomObject]@{
63
+ Name = '${value.replace(/'/g, "''")}'
64
+ Type = $kind.ToString()
65
+ Value = $raw
66
+ } | ConvertTo-Json -Depth 3 -Compress`;
67
+
68
+ const result = await runPowerShell(ps, { timeout: 10000 });
69
+ if (result.exitCode !== 0) {
70
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
71
+ }
72
+ const v = JSON.parse(result.stdout);
73
+ return { content: [{ type: 'text', text: `${regPath}\\${v.Name}\nType: ${v.Type}\nValue: ${JSON.stringify(v.Value)}` }] };
74
+ }
75
+
76
+ // List all values
77
+ const ps = `
78
+ $key = Get-Item -Path '${regPath.replace(/'/g, "''")}' -ErrorAction Stop
79
+ $key.GetValueNames() | ForEach-Object {
80
+ $name = $_
81
+ $val = $key.GetValue($name)
82
+ $kind = $key.GetValueKind($name)
83
+ [PSCustomObject]@{
84
+ Name = if ($name) { $name } else { '(Default)' }
85
+ Type = $kind.ToString()
86
+ Value = $val
87
+ }
88
+ } | ConvertTo-Json -Depth 3 -Compress`;
89
+
90
+ const result = await runPowerShell(ps, { timeout: 10000 });
91
+ if (result.exitCode !== 0) {
92
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
93
+ }
94
+ if (!result.stdout) {
95
+ return { content: [{ type: 'text', text: 'No values found.' }] };
96
+ }
97
+
98
+ const values = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
99
+ const lines = values.map((v: { Name: string; Type: string; Value: unknown }) => {
100
+ const valStr = typeof v.Value === 'string' ? v.Value : JSON.stringify(v.Value);
101
+ return ` ${v.Name.padEnd(30)} ${v.Type.padEnd(12)} ${valStr.slice(0, 60)}`;
102
+ });
103
+
104
+ const header = ` ${'Name'.padEnd(30)} ${'Type'.padEnd(12)} Value`;
105
+ return { content: [{ type: 'text', text: `${regPath}\n${header}\n${'─'.repeat(80)}\n${lines.join('\n')}` }] };
106
+ },
107
+ );
108
+
109
+ server.tool(
110
+ 'windows_registry_write',
111
+ 'Write Windows Registry values. Restricted to HKCU by default. Use hklm_override for HKLM writes.',
112
+ {
113
+ path: z.string().describe('Registry path'),
114
+ name: z.string().describe('Value name'),
115
+ value: z.string().describe('Value data'),
116
+ type: z.enum(['String', 'DWord', 'QWord', 'Binary', 'ExpandString', 'MultiString']).default('String').describe('Value type'),
117
+ hklm_override: z.boolean().default(false).describe('Allow writing to HKLM (requires elevation)'),
118
+ action: z.enum(['set', 'delete', 'create_key', 'delete_key']).default('set').describe('Action'),
119
+ },
120
+ async ({ path, name, value, type, hklm_override, action }) => {
121
+ const regPath = expandHive(path);
122
+
123
+ // Safety check
124
+ if (regPath.startsWith('HKLM:') && !hklm_override) {
125
+ return {
126
+ content: [{ type: 'text', text: 'HKLM writes are restricted. Set hklm_override=true and ensure elevation.' }],
127
+ isError: true,
128
+ };
129
+ }
130
+
131
+ let ps: string;
132
+
133
+ switch (action) {
134
+ case 'set': {
135
+ const typeMap: Record<string, string> = {
136
+ String: 'String', DWord: 'DWord', QWord: 'QWord',
137
+ Binary: 'Binary', ExpandString: 'ExpandString', MultiString: 'MultiString',
138
+ };
139
+ ps = `
140
+ if (-not (Test-Path '${regPath.replace(/'/g, "''")}')) {
141
+ New-Item -Path '${regPath.replace(/'/g, "''")}' -Force | Out-Null
142
+ }
143
+ Set-ItemProperty -Path '${regPath.replace(/'/g, "''")}' -Name '${name.replace(/'/g, "''")}' -Value '${value.replace(/'/g, "''")}' -Type ${typeMap[type]} -ErrorAction Stop
144
+ "Set ${regPath}\\${name} = ${value} (${type})"`;
145
+ break;
146
+ }
147
+ case 'delete':
148
+ ps = `Remove-ItemProperty -Path '${regPath.replace(/'/g, "''")}' -Name '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Deleted ${regPath}\\${name}"`;
149
+ break;
150
+ case 'create_key':
151
+ ps = `New-Item -Path '${regPath.replace(/'/g, "''")}' -Force -ErrorAction Stop | Out-Null; "Created key ${regPath}"`;
152
+ break;
153
+ case 'delete_key':
154
+ ps = `Remove-Item -Path '${regPath.replace(/'/g, "''")}' -Recurse -Force -ErrorAction Stop; "Deleted key ${regPath}"`;
155
+ break;
156
+ }
157
+
158
+ const result = await runPowerShell(ps, { timeout: 10000 });
159
+ return {
160
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
161
+ isError: result.exitCode !== 0,
162
+ };
163
+ },
164
+ );
165
+ }
@@ -0,0 +1,140 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_task_scheduler_list (#27), windows_task_scheduler_manage (#28)
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 registerSchedulerTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_task_scheduler_list',
14
+ 'List Windows Task Scheduler tasks with status, last/next run, and trigger type.',
15
+ {
16
+ folder: z.string().default('\\').describe('Task folder path (e.g. "\\" for root, "\\Microsoft\\")'),
17
+ filter: z.string().optional().describe('Filter by task name (substring)'),
18
+ },
19
+ async ({ folder, filter }) => {
20
+ const filterClause = filter
21
+ ? `| Where-Object { $_.TaskName -like '*${filter.replace(/'/g, "''")}*' }`
22
+ : '';
23
+
24
+ const ps = `
25
+ Get-ScheduledTask -TaskPath '${folder.replace(/'/g, "''")}*' -ErrorAction SilentlyContinue ${filterClause} | ForEach-Object {
26
+ $info = $_ | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue
27
+ [PSCustomObject]@{
28
+ Name = $_.TaskName
29
+ Path = $_.TaskPath
30
+ State = $_.State.ToString()
31
+ LastRun = if ($info.LastRunTime -and $info.LastRunTime.Year -gt 1999) { $info.LastRunTime.ToString('yyyy-MM-dd HH:mm') } else { 'Never' }
32
+ NextRun = if ($info.NextRunTime -and $info.NextRunTime.Year -gt 1999) { $info.NextRunTime.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' }
33
+ LastResult = if ($info) { '0x{0:X}' -f $info.LastTaskResult } else { 'N/A' }
34
+ Triggers = ($_.Triggers | ForEach-Object { $_.CimClass.CimClassName -replace 'MSFT_Task',''-replace 'Trigger','' }) -join ', '
35
+ }
36
+ } | ConvertTo-Json -Depth 3 -Compress`;
37
+
38
+ const result = await runPowerShell(ps, { timeout: 30000 });
39
+ if (result.exitCode !== 0) {
40
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
41
+ }
42
+
43
+ if (!result.stdout) {
44
+ return { content: [{ type: 'text', text: 'No tasks found.' }] };
45
+ }
46
+
47
+ const tasks = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
48
+ const lines = tasks.map((t: { Name: string; State: string; LastRun: string; NextRun: string; Triggers: string }) => {
49
+ const state = t.State === 'Ready' ? '[RDY]' : t.State === 'Running' ? '[RUN]' : t.State === 'Disabled' ? '[OFF]' : `[${t.State.slice(0, 3).toUpperCase()}]`;
50
+ return `${state} ${t.Name.padEnd(40).slice(0, 40)} ${t.LastRun.padEnd(16)} ${t.NextRun.padEnd(16)} ${t.Triggers}`;
51
+ });
52
+
53
+ const header = `State ${'Name'.padEnd(40)} ${'Last Run'.padEnd(16)} ${'Next Run'.padEnd(16)} Triggers`;
54
+ return {
55
+ content: [{ type: 'text', text: `${header}\n${'─'.repeat(110)}\n${lines.join('\n')}\n\n${tasks.length} tasks` }],
56
+ };
57
+ },
58
+ );
59
+
60
+ server.tool(
61
+ 'windows_task_scheduler_manage',
62
+ 'Create, delete, enable, disable, or run a scheduled task.',
63
+ {
64
+ action: z.enum(['create', 'delete', 'enable', 'disable', 'run']).describe('Action to perform'),
65
+ name: z.string().describe('Task name'),
66
+ command: z.string().optional().describe('Command to execute (for create)'),
67
+ arguments: z.string().optional().describe('Command arguments (for create)'),
68
+ trigger: z.enum(['once', 'daily', 'weekly', 'hourly', 'logon', 'startup']).optional().describe('Trigger type (for create)'),
69
+ time: z.string().optional().describe('Time for trigger as HH:mm (for create with once/daily/weekly)'),
70
+ interval: z.number().optional().describe('Repetition interval in minutes (for create with hourly)'),
71
+ },
72
+ async ({ action, name, command, arguments: args, trigger, time, interval }) => {
73
+ let ps: string;
74
+
75
+ switch (action) {
76
+ case 'run':
77
+ ps = `Start-ScheduledTask -TaskName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Task '${name}' started"`;
78
+ break;
79
+ case 'enable':
80
+ ps = `Enable-ScheduledTask -TaskName '${name.replace(/'/g, "''")}' -ErrorAction Stop | Select-Object TaskName,State | ConvertTo-Json -Compress`;
81
+ break;
82
+ case 'disable':
83
+ ps = `Disable-ScheduledTask -TaskName '${name.replace(/'/g, "''")}' -ErrorAction Stop | Select-Object TaskName,State | ConvertTo-Json -Compress`;
84
+ break;
85
+ case 'delete':
86
+ ps = `Unregister-ScheduledTask -TaskName '${name.replace(/'/g, "''")}' -Confirm:$false -ErrorAction Stop; "Task '${name}' deleted"`;
87
+ break;
88
+ case 'create': {
89
+ if (!command) {
90
+ return { content: [{ type: 'text', text: 'Create requires command.' }], isError: true };
91
+ }
92
+ if (!trigger) {
93
+ return { content: [{ type: 'text', text: 'Create requires trigger type.' }], isError: true };
94
+ }
95
+
96
+ const actionPart = args
97
+ ? `$action = New-ScheduledTaskAction -Execute '${command.replace(/'/g, "''")}' -Argument '${args.replace(/'/g, "''")}'`
98
+ : `$action = New-ScheduledTaskAction -Execute '${command.replace(/'/g, "''")}'`;
99
+
100
+ let triggerPart: string;
101
+ switch (trigger) {
102
+ case 'once':
103
+ triggerPart = `$trigger = New-ScheduledTaskTrigger -Once -At '${time || '00:00'}'`;
104
+ break;
105
+ case 'daily':
106
+ triggerPart = `$trigger = New-ScheduledTaskTrigger -Daily -At '${time || '00:00'}'`;
107
+ break;
108
+ case 'weekly':
109
+ triggerPart = `$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At '${time || '00:00'}'`;
110
+ break;
111
+ case 'hourly':
112
+ triggerPart = `$trigger = New-ScheduledTaskTrigger -Once -At '00:00' -RepetitionInterval (New-TimeSpan -Minutes ${interval || 60}) -RepetitionDuration (New-TimeSpan -Days 9999)`;
113
+ break;
114
+ case 'logon':
115
+ triggerPart = `$trigger = New-ScheduledTaskTrigger -AtLogOn`;
116
+ break;
117
+ case 'startup':
118
+ triggerPart = `$trigger = New-ScheduledTaskTrigger -AtStartup`;
119
+ break;
120
+ }
121
+
122
+ ps = `
123
+ ${actionPart}
124
+ ${triggerPart}
125
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
126
+ Register-ScheduledTask -TaskName '${name.replace(/'/g, "''")}' -Action $action -Trigger $trigger -Settings $settings -Force -ErrorAction Stop |
127
+ Select-Object TaskName,State | ConvertTo-Json -Compress`;
128
+ break;
129
+ }
130
+ }
131
+
132
+ const result = await runPowerShell(ps, { timeout: 15000 });
133
+ if (result.exitCode !== 0) {
134
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
135
+ }
136
+
137
+ return { content: [{ type: 'text', text: result.stdout }] };
138
+ },
139
+ );
140
+ }
@@ -0,0 +1,102 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_service_list (#4), windows_service_control (#5)
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 registerServiceTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_service_list',
14
+ 'List Windows services with status, startup type, and description.',
15
+ {
16
+ filter: z.string().optional().describe('Filter by service name or display name (substring)'),
17
+ status: z.enum(['running', 'stopped', 'all']).default('all').describe('Filter by status'),
18
+ },
19
+ async ({ filter, status }) => {
20
+ const filterClause = filter
21
+ ? `| Where-Object { $_.Name -like '*${filter.replace(/'/g, "''") }*' -or $_.DisplayName -like '*${filter.replace(/'/g, "''")}*' }`
22
+ : '';
23
+ const statusClause = status === 'running'
24
+ ? `| Where-Object { $_.Status -eq 'Running' }`
25
+ : status === 'stopped'
26
+ ? `| Where-Object { $_.Status -eq 'Stopped' }`
27
+ : '';
28
+
29
+ const ps = `
30
+ $wmiCache = @{}
31
+ Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | ForEach-Object { $wmiCache[$_.Name] = $_.Description }
32
+ Get-Service ${filterClause} ${statusClause} | Sort-Object DisplayName | ForEach-Object {
33
+ [PSCustomObject]@{
34
+ Name = $_.Name
35
+ DisplayName = $_.DisplayName
36
+ Status = $_.Status.ToString()
37
+ StartType = $_.StartType.ToString()
38
+ Description = if ($wmiCache[$_.Name]) { $wmiCache[$_.Name] } else { '' }
39
+ }
40
+ } | ConvertTo-Json -Depth 3 -Compress`;
41
+
42
+ const result = await runPowerShell(ps, { timeout: 45000 });
43
+ if (result.exitCode !== 0) {
44
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
45
+ }
46
+
47
+ if (!result.stdout) {
48
+ return { content: [{ type: 'text', text: 'No services found.' }] };
49
+ }
50
+
51
+ const services = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
52
+ const lines = services.map((s: { Name: string; DisplayName: string; Status: string; StartType: string }) =>
53
+ `${s.Status === 'Running' ? '[RUN]' : '[STP]'} ${s.StartType.padEnd(10)} ${s.Name.padEnd(35).slice(0, 35)} ${s.DisplayName}`,
54
+ );
55
+
56
+ const header = `${'State'.padEnd(5)} ${'Startup'.padEnd(10)} ${'Name'.padEnd(35)} Display Name`;
57
+ return {
58
+ content: [{ type: 'text', text: `${header}\n${'─'.repeat(100)}\n${lines.join('\n')}\n\n${services.length} services` }],
59
+ };
60
+ },
61
+ );
62
+
63
+ server.tool(
64
+ 'windows_service_control',
65
+ 'Start, stop, restart, or change startup type of a Windows service.',
66
+ {
67
+ name: z.string().describe('Service name or display name'),
68
+ action: z.enum(['start', 'stop', 'restart', 'enable', 'disable']).describe('Action to perform'),
69
+ },
70
+ async ({ name, action }) => {
71
+ let ps: string;
72
+
73
+ switch (action) {
74
+ case 'start':
75
+ ps = `Start-Service -Name '${name.replace(/'/g, "''")}' -ErrorAction Stop; Get-Service -Name '${name.replace(/'/g, "''")}' | Select-Object Name,Status,StartType | ConvertTo-Json -Compress`;
76
+ break;
77
+ case 'stop':
78
+ ps = `Stop-Service -Name '${name.replace(/'/g, "''")}' -Force -ErrorAction Stop; Get-Service -Name '${name.replace(/'/g, "''")}' | Select-Object Name,Status,StartType | ConvertTo-Json -Compress`;
79
+ break;
80
+ case 'restart':
81
+ ps = `Restart-Service -Name '${name.replace(/'/g, "''")}' -Force -ErrorAction Stop; Get-Service -Name '${name.replace(/'/g, "''")}' | Select-Object Name,Status,StartType | ConvertTo-Json -Compress`;
82
+ break;
83
+ case 'enable':
84
+ ps = `Set-Service -Name '${name.replace(/'/g, "''")}' -StartupType Automatic -ErrorAction Stop; Get-Service -Name '${name.replace(/'/g, "''")}' | Select-Object Name,Status,StartType | ConvertTo-Json -Compress`;
85
+ break;
86
+ case 'disable':
87
+ ps = `Set-Service -Name '${name.replace(/'/g, "''")}' -StartupType Disabled -ErrorAction Stop; Get-Service -Name '${name.replace(/'/g, "''")}' | Select-Object Name,Status,StartType | ConvertTo-Json -Compress`;
88
+ break;
89
+ }
90
+
91
+ const result = await runPowerShell(ps, { timeout: 15000 });
92
+ if (result.exitCode !== 0) {
93
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
94
+ }
95
+
96
+ const svc = JSON.parse(result.stdout);
97
+ return {
98
+ content: [{ type: 'text', text: `Service "${svc.Name}": ${action} → Status: ${svc.Status}, StartType: ${svc.StartType}` }],
99
+ };
100
+ },
101
+ );
102
+ }
@@ -0,0 +1,180 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_startup_list (#33), windows_startup_manage (#34)
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 registerStartupTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_startup_list',
14
+ 'List all startup items from registry (Run/RunOnce), startup folder, and scheduled logon tasks.',
15
+ {},
16
+ async () => {
17
+ const ps = `
18
+ $items = [System.Collections.Generic.List[PSObject]]::new()
19
+
20
+ # Registry: HKCU Run
21
+ $regPaths = @(
22
+ @{ Path = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'; Scope = 'User'; Source = 'Registry Run' }
23
+ @{ Path = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce'; Scope = 'User'; Source = 'Registry RunOnce' }
24
+ @{ Path = 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'; Scope = 'System'; Source = 'Registry Run' }
25
+ @{ Path = 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce'; Scope = 'System'; Source = 'Registry RunOnce' }
26
+ )
27
+
28
+ foreach ($rp in $regPaths) {
29
+ if (Test-Path $rp.Path) {
30
+ $props = Get-ItemProperty -Path $rp.Path -ErrorAction SilentlyContinue
31
+ if ($props) {
32
+ $props.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } | ForEach-Object {
33
+ $items.Add([PSCustomObject]@{
34
+ Name = $_.Name
35
+ Command = $_.Value
36
+ Source = $rp.Source
37
+ Scope = $rp.Scope
38
+ Enabled = $true
39
+ })
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ # Startup folder
46
+ $userStartup = [Environment]::GetFolderPath('Startup')
47
+ $commonStartup = [Environment]::GetFolderPath('CommonStartup')
48
+ foreach ($folder in @(@{Path=$userStartup;Scope='User'}, @{Path=$commonStartup;Scope='System'})) {
49
+ if (Test-Path $folder.Path) {
50
+ Get-ChildItem -Path $folder.Path -File -ErrorAction SilentlyContinue | ForEach-Object {
51
+ $items.Add([PSCustomObject]@{
52
+ Name = $_.BaseName
53
+ Command = $_.FullName
54
+ Source = 'Startup Folder'
55
+ Scope = $folder.Scope
56
+ Enabled = $true
57
+ })
58
+ }
59
+ }
60
+ }
61
+
62
+ # Scheduled tasks at logon
63
+ Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object {
64
+ $_.Triggers | Where-Object { $_ -is [CimInstance] -and $_.CimClass.CimClassName -eq 'MSFT_TaskLogonTrigger' }
65
+ } | ForEach-Object {
66
+ $items.Add([PSCustomObject]@{
67
+ Name = $_.TaskName
68
+ Command = ($_.Actions | ForEach-Object { $_.Execute }) -join ' '
69
+ Source = 'Task Scheduler (Logon)'
70
+ Scope = 'System'
71
+ Enabled = $_.State -eq 'Ready'
72
+ })
73
+ }
74
+
75
+ $items | ConvertTo-Json -Depth 3 -Compress`;
76
+
77
+ const result = await runPowerShell(ps, { timeout: 20000 });
78
+ if (result.exitCode !== 0) {
79
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
80
+ }
81
+
82
+ if (!result.stdout) {
83
+ return { content: [{ type: 'text', text: 'No startup items found.' }] };
84
+ }
85
+
86
+ const items = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
87
+ const lines = items.map((i: { Name: string; Command: string; Source: string; Scope: string; Enabled: boolean }) => {
88
+ const status = i.Enabled ? '[ON] ' : '[OFF]';
89
+ return `${status} ${i.Scope.padEnd(6)} ${i.Source.padEnd(24)} ${i.Name.padEnd(30).slice(0, 30)} ${(i.Command || '').slice(0, 50)}`;
90
+ });
91
+
92
+ const header = `State ${'Scope'.padEnd(6)} ${'Source'.padEnd(24)} ${'Name'.padEnd(30)} Command`;
93
+ return {
94
+ content: [{ type: 'text', text: `${header}\n${'─'.repeat(110)}\n${lines.join('\n')}\n\n${items.length} startup items` }],
95
+ };
96
+ },
97
+ );
98
+
99
+ server.tool(
100
+ 'windows_startup_manage',
101
+ 'Add, remove, enable, or disable a startup item.',
102
+ {
103
+ action: z.enum(['add', 'remove', 'enable', 'disable']).describe('Action'),
104
+ name: z.string().describe('Startup item name'),
105
+ command: z.string().optional().describe('Command to run at startup (for add)'),
106
+ location: z.enum(['registry', 'startup_folder']).default('registry').describe('Where to add (for add)'),
107
+ },
108
+ async ({ action, name, command, location }) => {
109
+ let ps: string;
110
+
111
+ switch (action) {
112
+ case 'add':
113
+ if (!command) {
114
+ return { content: [{ type: 'text', text: 'Add requires a command.' }], isError: true };
115
+ }
116
+ if (location === 'startup_folder') {
117
+ ps = `
118
+ $startupPath = [Environment]::GetFolderPath('Startup')
119
+ $shortcutPath = Join-Path $startupPath '${name.replace(/'/g, "''")}.lnk'
120
+ $ws = New-Object -ComObject WScript.Shell
121
+ $sc = $ws.CreateShortcut($shortcutPath)
122
+ $sc.TargetPath = '${command.replace(/'/g, "''")}'
123
+ $sc.Save()
124
+ "Added startup shortcut: $shortcutPath"`;
125
+ } else {
126
+ ps = `
127
+ Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${name.replace(/'/g, "''")}' -Value '${command.replace(/'/g, "''")}' -ErrorAction Stop
128
+ "Added to HKCU Run: ${name}"`;
129
+ }
130
+ break;
131
+
132
+ case 'remove':
133
+ ps = `
134
+ $removed = $false
135
+ # Try registry
136
+ $regPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
137
+ if ((Get-ItemProperty -Path $regPath -Name '${name.replace(/'/g, "''")}' -ErrorAction SilentlyContinue)) {
138
+ Remove-ItemProperty -Path $regPath -Name '${name.replace(/'/g, "''")}' -ErrorAction Stop
139
+ $removed = $true
140
+ }
141
+ # Try startup folder
142
+ $startupPath = [Environment]::GetFolderPath('Startup')
143
+ $lnk = Join-Path $startupPath '${name.replace(/'/g, "''")}.lnk'
144
+ if (Test-Path $lnk) { Remove-Item $lnk -Force; $removed = $true }
145
+ if ($removed) { "Removed: ${name}" } else { "Not found: ${name}" }`;
146
+ break;
147
+
148
+ case 'disable':
149
+ ps = `
150
+ $regPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
151
+ $val = (Get-ItemProperty -Path $regPath -Name '${name.replace(/'/g, "''")}' -ErrorAction SilentlyContinue).'${name.replace(/'/g, "''")}'
152
+ if ($val) {
153
+ Remove-ItemProperty -Path $regPath -Name '${name.replace(/'/g, "''")}' -ErrorAction Stop
154
+ $disabledPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run_Disabled'
155
+ if (-not (Test-Path $disabledPath)) { New-Item -Path $disabledPath -Force | Out-Null }
156
+ Set-ItemProperty -Path $disabledPath -Name '${name.replace(/'/g, "''")}' -Value $val
157
+ "Disabled: ${name}"
158
+ } else { "Not found in registry Run: ${name}" }`;
159
+ break;
160
+
161
+ case 'enable':
162
+ ps = `
163
+ $disabledPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run_Disabled'
164
+ $val = (Get-ItemProperty -Path $disabledPath -Name '${name.replace(/'/g, "''")}' -ErrorAction SilentlyContinue).'${name.replace(/'/g, "''")}'
165
+ if ($val) {
166
+ Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${name.replace(/'/g, "''")}' -Value $val
167
+ Remove-ItemProperty -Path $disabledPath -Name '${name.replace(/'/g, "''")}' -ErrorAction SilentlyContinue
168
+ "Enabled: ${name}"
169
+ } else { "Not found in disabled items: ${name}" }`;
170
+ break;
171
+ }
172
+
173
+ const result = await runPowerShell(ps, { timeout: 10000 });
174
+ return {
175
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
176
+ isError: result.exitCode !== 0,
177
+ };
178
+ },
179
+ );
180
+ }