@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,141 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_disk_cleanup (#72), windows_symlink (#73)
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 registerStorageTools(server: McpServer): void {
12
+ server.tool(
13
+ 'windows_disk_cleanup',
14
+ 'Analyze disk usage and clean temp files, caches, and logs.',
15
+ {
16
+ action: z.enum(['analyze', 'clean']).default('analyze').describe('Analyze or clean'),
17
+ drive: z.string().default('C:').describe('Drive letter'),
18
+ },
19
+ async ({ action, drive }) => {
20
+ if (action === 'analyze') {
21
+ const ps = `
22
+ $tempUser = [IO.Path]::GetTempPath()
23
+ $tempWin = "$env:WINDIR\\Temp"
24
+ $downloads = [Environment]::GetFolderPath('UserProfile') + '\\Downloads'
25
+
26
+ function Get-FolderSize($path) {
27
+ if (Test-Path $path) {
28
+ $size = (Get-ChildItem $path -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
29
+ [math]::Round($size / 1MB, 1)
30
+ } else { 0 }
31
+ }
32
+
33
+ $disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='${drive.replace(/'/g, "''")}'"
34
+
35
+ [PSCustomObject]@{
36
+ Drive = '${drive}'
37
+ TotalGB = [math]::Round($disk.Size / 1GB, 1)
38
+ FreeGB = [math]::Round($disk.FreeSpace / 1GB, 1)
39
+ UsedPct = [math]::Round(($disk.Size - $disk.FreeSpace) / $disk.Size * 100, 1)
40
+ TempUserMB = Get-FolderSize $tempUser
41
+ TempWindowsMB = Get-FolderSize $tempWin
42
+ DownloadsMB = Get-FolderSize $downloads
43
+ RecycleBinItems = (New-Object -ComObject Shell.Application).Namespace(10).Items().Count
44
+ } | ConvertTo-Json -Compress`;
45
+
46
+ const result = await runPowerShell(ps, { timeout: 30000 });
47
+ if (result.exitCode !== 0) {
48
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
49
+ }
50
+ const d = JSON.parse(result.stdout);
51
+ return {
52
+ content: [{
53
+ type: 'text',
54
+ text: [
55
+ `${d.Drive} — ${d.FreeGB}/${d.TotalGB} GB free (${d.UsedPct}% used)`,
56
+ ``,
57
+ `Cleanable:`,
58
+ ` User temp: ${d.TempUserMB} MB`,
59
+ ` Windows temp: ${d.TempWindowsMB} MB`,
60
+ ` Downloads: ${d.DownloadsMB} MB`,
61
+ ` Recycle Bin: ${d.RecycleBinItems} items`,
62
+ ].join('\n'),
63
+ }],
64
+ };
65
+ }
66
+
67
+ const ps = `
68
+ $before = (Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='${drive.replace(/'/g, "''")}'").FreeSpace
69
+ Remove-Item "$env:TEMP\\*" -Recurse -Force -ErrorAction SilentlyContinue
70
+ Remove-Item "$env:WINDIR\\Temp\\*" -Recurse -Force -ErrorAction SilentlyContinue
71
+ $after = (Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='${drive.replace(/'/g, "''")}'").FreeSpace
72
+ $freed = [math]::Round(($after - $before) / 1MB, 1)
73
+ "Cleaned temp files. Freed: $freed MB"`;
74
+
75
+ const result = await runPowerShell(ps, { timeout: 30000 });
76
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
77
+ },
78
+ );
79
+
80
+ server.tool(
81
+ 'windows_symlink',
82
+ 'Create and manage symbolic links, hard links, and directory junctions.',
83
+ {
84
+ action: z.enum(['create', 'list', 'resolve', 'remove']).default('list').describe('Action'),
85
+ target: z.string().optional().describe('Target path (what the link points to)'),
86
+ link: z.string().optional().describe('Link path (the symlink itself)'),
87
+ type: z.enum(['symlink', 'junction', 'hardlink']).default('symlink').describe('Link type (for create)'),
88
+ path: z.string().optional().describe('Directory to list symlinks in (for list)'),
89
+ },
90
+ async ({ action, target, link, type, path }) => {
91
+ switch (action) {
92
+ case 'create': {
93
+ if (!target || !link) {
94
+ return { content: [{ type: 'text', text: 'create requires target and link.' }], isError: true };
95
+ }
96
+ let cmd: string;
97
+ if (type === 'junction') {
98
+ cmd = `cmd /c mklink /J "${link}" "${target}"`;
99
+ } else if (type === 'hardlink') {
100
+ cmd = `cmd /c mklink /H "${link}" "${target}"`;
101
+ } else {
102
+ cmd = `cmd /c mklink ${target.includes('.') ? '' : '/D'} "${link}" "${target}"`;
103
+ }
104
+ const ps = `& { ${cmd} } 2>&1`;
105
+ const result = await runPowerShell(ps, { timeout: 10000 });
106
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
107
+ }
108
+ case 'list': {
109
+ const dir = path || '.';
110
+ const ps = `
111
+ Get-ChildItem -Path '${dir.replace(/'/g, "''")}' -Force -ErrorAction Stop | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint } | ForEach-Object {
112
+ [PSCustomObject]@{
113
+ Name = $_.Name
114
+ Target = $_.Target
115
+ Type = if ($_.PSIsContainer) { 'Directory' } else { 'File' }
116
+ LinkType = $_.LinkType
117
+ }
118
+ } | ConvertTo-Json -Depth 3 -Compress`;
119
+ const result = await runPowerShell(ps, { timeout: 10000 });
120
+ if (!result.stdout) return { content: [{ type: 'text', text: 'No symlinks found.' }] };
121
+ const links = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
122
+ const lines = links.map((l: { Name: string; Target: string; LinkType: string }) =>
123
+ ` ${(l.LinkType || 'Link').padEnd(12)} ${l.Name.padEnd(30)} -> ${l.Target}`);
124
+ return { content: [{ type: 'text', text: `Symlinks in ${dir}:\n${lines.join('\n')}` }] };
125
+ }
126
+ case 'resolve': {
127
+ if (!link) return { content: [{ type: 'text', text: 'resolve requires link path.' }], isError: true };
128
+ const ps = `(Get-Item '${link.replace(/'/g, "''")}' -Force).Target`;
129
+ const result = await runPowerShell(ps, { timeout: 5000 });
130
+ return { content: [{ type: 'text', text: `${link} -> ${result.stdout || '(not a symlink)'}` }] };
131
+ }
132
+ case 'remove': {
133
+ if (!link) return { content: [{ type: 'text', text: 'remove requires link path.' }], isError: true };
134
+ const ps = `Remove-Item '${link.replace(/'/g, "''")}' -Force -ErrorAction Stop; "Removed: ${link}"`;
135
+ const result = await runPowerShell(ps, { timeout: 5000 });
136
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
137
+ }
138
+ }
139
+ },
140
+ );
141
+ }
@@ -0,0 +1,99 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_system_info (#18)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { runPowerShell } from '../shell.js';
9
+
10
+ export function registerSystemTools(server: McpServer): void {
11
+ server.tool(
12
+ 'windows_system_info',
13
+ 'Get comprehensive system information: OS, CPU, RAM, disk, network, uptime.',
14
+ {},
15
+ async () => {
16
+ const ps = `
17
+ $os = Get-CimInstance Win32_OperatingSystem
18
+ $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
19
+ $cs = Get-CimInstance Win32_ComputerSystem
20
+ $disks = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
21
+ [PSCustomObject]@{
22
+ Drive = $_.DeviceID
23
+ Label = $_.VolumeName
24
+ FileSystem = $_.FileSystem
25
+ TotalGB = [math]::Round($_.Size / 1GB, 1)
26
+ FreeGB = [math]::Round($_.FreeSpace / 1GB, 1)
27
+ UsedPct = if ($_.Size -gt 0) { [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1) } else { 0 }
28
+ }
29
+ }
30
+ $adapters = Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'Up' } | ForEach-Object {
31
+ $ip = (Get-NetIPAddress -InterfaceIndex $_.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue).IPAddress
32
+ [PSCustomObject]@{
33
+ Name = $_.Name
34
+ Speed = $_.LinkSpeed
35
+ IP = $ip
36
+ MAC = $_.MacAddress
37
+ }
38
+ }
39
+ $uptime = (Get-Date) - $os.LastBootUpTime
40
+
41
+ [PSCustomObject]@{
42
+ OS = "$($os.Caption) $($os.Version) Build $($os.BuildNumber)"
43
+ Edition = $os.OperatingSystemSKU
44
+ Architecture = $os.OSArchitecture
45
+ Hostname = $env:COMPUTERNAME
46
+ Username = $env:USERNAME
47
+ Domain = $cs.Domain
48
+ CPU = "$($cpu.Name)"
49
+ CPUCores = "$($cpu.NumberOfCores) cores / $($cpu.NumberOfLogicalProcessors) threads"
50
+ CPUUsage = "$([math]::Round($cpu.LoadPercentage, 0))%"
51
+ RAMTotalGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
52
+ RAMAvailGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
53
+ RAMUsedPct = [math]::Round(($cs.TotalPhysicalMemory - $os.FreePhysicalMemory * 1KB) / $cs.TotalPhysicalMemory * 100, 1)
54
+ Disks = $disks
55
+ Network = $adapters
56
+ Uptime = "$($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m"
57
+ } | ConvertTo-Json -Depth 4 -Compress`;
58
+
59
+ const result = await runPowerShell(ps, { timeout: 45000 });
60
+ if (result.exitCode !== 0) {
61
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
62
+ }
63
+
64
+ const info = JSON.parse(result.stdout);
65
+ const diskLines = (Array.isArray(info.Disks) ? info.Disks : [info.Disks])
66
+ .filter(Boolean)
67
+ .map((d: { Drive: string; Label: string; TotalGB: number; FreeGB: number; UsedPct: number }) =>
68
+ ` ${d.Drive} ${d.Label || ''} — ${d.FreeGB}/${d.TotalGB} GB free (${d.UsedPct}% used)`,
69
+ );
70
+ const netLines = (Array.isArray(info.Network) ? info.Network : [info.Network])
71
+ .filter(Boolean)
72
+ .map((n: { Name: string; IP: string; Speed: string }) =>
73
+ ` ${n.Name} — ${n.IP || 'no IP'} (${n.Speed})`,
74
+ );
75
+
76
+ const text = [
77
+ `OS: ${info.OS}`,
78
+ `Architecture: ${info.Architecture}`,
79
+ `Host: ${info.Hostname} (${info.Domain})`,
80
+ `User: ${info.Username}`,
81
+ `Uptime: ${info.Uptime}`,
82
+ ``,
83
+ `CPU: ${info.CPU}`,
84
+ `Cores: ${info.CPUCores}`,
85
+ `CPU Usage: ${info.CPUUsage}`,
86
+ ``,
87
+ `RAM: ${info.RAMAvailGB}/${info.RAMTotalGB} GB available (${info.RAMUsedPct}% used)`,
88
+ ``,
89
+ `Disks:`,
90
+ ...diskLines,
91
+ ``,
92
+ `Network:`,
93
+ ...netLines,
94
+ ].join('\n');
95
+
96
+ return { content: [{ type: 'text', text }] };
97
+ },
98
+ );
99
+ }
@@ -0,0 +1,190 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_timezone (#74), windows_features (#75),
5
+ * windows_smb_shares (#76), windows_dns_cache (#77)
6
+ */
7
+
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { z } from 'zod';
10
+ import { runPowerShell } from '../shell.js';
11
+
12
+ export function registerSystemMgmtTools(server: McpServer): void {
13
+ server.tool(
14
+ 'windows_timezone',
15
+ 'Get or set system timezone. List available timezones or sync time.',
16
+ {
17
+ action: z.enum(['get', 'list', 'set', 'sync']).default('get').describe('Action'),
18
+ timezone: z.string().optional().describe('Timezone ID (for set, e.g. "Eastern Standard Time")'),
19
+ filter: z.string().optional().describe('Filter timezone list'),
20
+ },
21
+ async ({ action, timezone, filter }) => {
22
+ switch (action) {
23
+ case 'get': {
24
+ const ps = `
25
+ $tz = Get-TimeZone
26
+ [PSCustomObject]@{
27
+ Id = $tz.Id
28
+ DisplayName = $tz.DisplayName
29
+ UTCOffset = $tz.BaseUtcOffset.ToString()
30
+ DST = $tz.SupportsDaylightSavingTime
31
+ DSTActive = (Get-Date).IsDaylightSavingTime()
32
+ CurrentTime = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
33
+ UTCTime = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss')
34
+ } | ConvertTo-Json -Compress`;
35
+ const result = await runPowerShell(ps, { timeout: 10000 });
36
+ if (result.exitCode !== 0) return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
37
+ const tz = JSON.parse(result.stdout);
38
+ return {
39
+ content: [{
40
+ type: 'text',
41
+ text: `Timezone: ${tz.Id}\n${tz.DisplayName}\nUTC Offset: ${tz.UTCOffset}${tz.DSTActive ? ' (DST active)' : ''}\nLocal: ${tz.CurrentTime}\nUTC: ${tz.UTCTime}`,
42
+ }],
43
+ };
44
+ }
45
+ case 'list': {
46
+ const filterClause = filter ? `| Where-Object { $_.Id -like '*${filter.replace(/'/g, "''")}*' -or $_.DisplayName -like '*${filter.replace(/'/g, "''")}*' }` : '';
47
+ const ps = `Get-TimeZone -ListAvailable ${filterClause} | ForEach-Object { "$($_.BaseUtcOffset.ToString().PadRight(9)) $($_.Id)" }`;
48
+ const result = await runPowerShell(ps, { timeout: 10000 });
49
+ return { content: [{ type: 'text', text: result.stdout || 'No timezones found.' }] };
50
+ }
51
+ case 'set': {
52
+ if (!timezone) return { content: [{ type: 'text', text: 'set requires timezone.' }], isError: true };
53
+ const ps = `Set-TimeZone -Id '${timezone.replace(/'/g, "''")}' -ErrorAction Stop; "Timezone set to: ${timezone}"`;
54
+ const result = await runPowerShell(ps, { timeout: 10000 });
55
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
56
+ }
57
+ case 'sync': {
58
+ const ps = `w32tm /resync /force 2>&1; "Time sync requested"`;
59
+ const result = await runPowerShell(ps, { timeout: 15000 });
60
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
61
+ }
62
+ }
63
+ },
64
+ );
65
+
66
+ server.tool(
67
+ 'windows_features',
68
+ 'List, enable, or disable Windows optional features (WSL, Hyper-V, Sandbox, etc.).',
69
+ {
70
+ action: z.enum(['list', 'enable', 'disable']).default('list').describe('Action'),
71
+ name: z.string().optional().describe('Feature name (for enable/disable)'),
72
+ filter: z.string().optional().describe('Filter by name (for list)'),
73
+ },
74
+ async ({ action, name, filter }) => {
75
+ if (action === 'list') {
76
+ const filterClause = filter ? `| Where-Object { $_.FeatureName -like '*${filter.replace(/'/g, "''")}*' }` : '';
77
+ const ps = `
78
+ try {
79
+ Get-WindowsOptionalFeature -Online -ErrorAction Stop ${filterClause} | Select-Object FeatureName,State | Sort-Object FeatureName | ConvertTo-Json -Depth 3 -Compress
80
+ } catch {
81
+ if ($_.Exception.Message -match 'elevation') { Write-Output 'NEEDS_ELEVATION' }
82
+ else { throw }
83
+ }`;
84
+ const result = await runPowerShell(ps, { timeout: 30000 });
85
+ if (result.stdout?.trim() === 'NEEDS_ELEVATION') {
86
+ return { content: [{ type: 'text', text: 'Windows Optional Features requires elevation (run as Administrator).' }] };
87
+ }
88
+ if (result.exitCode !== 0) return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
89
+ if (!result.stdout) return { content: [{ type: 'text', text: 'No features found.' }] };
90
+ const features = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
91
+ const lines = features.map((f: { FeatureName: string; State: number }) => {
92
+ const state = f.State === 2 ? '[ON] ' : '[OFF]';
93
+ return `${state} ${f.FeatureName}`;
94
+ });
95
+ return { content: [{ type: 'text', text: `${lines.join('\n')}\n\n${features.length} features` }] };
96
+ }
97
+
98
+ if (!name) return { content: [{ type: 'text', text: `${action} requires name.` }], isError: true };
99
+ const cmd = action === 'enable'
100
+ ? `Enable-WindowsOptionalFeature -Online -FeatureName '${name.replace(/'/g, "''")}' -NoRestart -ErrorAction Stop`
101
+ : `Disable-WindowsOptionalFeature -Online -FeatureName '${name.replace(/'/g, "''")}' -NoRestart -ErrorAction Stop`;
102
+ const ps = `${cmd} | Select-Object RestartNeeded | ConvertTo-Json -Compress`;
103
+ const result = await runPowerShell(ps, { timeout: 60000 });
104
+ if (result.exitCode !== 0) return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
105
+ const info = JSON.parse(result.stdout);
106
+ return { content: [{ type: 'text', text: `${name}: ${action}d${info.RestartNeeded ? ' (restart required)' : ''}` }] };
107
+ },
108
+ );
109
+
110
+ server.tool(
111
+ 'windows_smb_shares',
112
+ 'List local shares, mapped drives, or map/unmap network drives.',
113
+ {
114
+ action: z.enum(['list_shares', 'list_mapped', 'map', 'unmap']).default('list_shares').describe('Action'),
115
+ letter: z.string().optional().describe('Drive letter (for map/unmap, e.g. "Z:")'),
116
+ unc_path: z.string().optional().describe('UNC path (for map, e.g. "\\\\\\\\server\\\\share")'),
117
+ },
118
+ async ({ action, letter, unc_path }) => {
119
+ switch (action) {
120
+ case 'list_shares': {
121
+ const ps = `Get-SmbShare -ErrorAction Stop | Select-Object Name,Path,Description | ConvertTo-Json -Depth 3 -Compress`;
122
+ const result = await runPowerShell(ps, { timeout: 10000 });
123
+ if (result.exitCode !== 0) return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
124
+ if (!result.stdout) return { content: [{ type: 'text', text: 'No shares.' }] };
125
+ const shares = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
126
+ const lines = shares.map((s: { Name: string; Path: string; Description: string }) =>
127
+ ` ${s.Name.padEnd(20)} ${(s.Path || '').padEnd(30)} ${s.Description || ''}`);
128
+ return { content: [{ type: 'text', text: `Local shares:\n${lines.join('\n')}` }] };
129
+ }
130
+ case 'list_mapped': {
131
+ const ps = `Get-PSDrive -PSProvider FileSystem | Where-Object { $_.DisplayRoot } | ForEach-Object { "$($_.Name): $($_.DisplayRoot)" }`;
132
+ const result = await runPowerShell(ps, { timeout: 10000 });
133
+ return { content: [{ type: 'text', text: result.stdout || 'No mapped drives.' }] };
134
+ }
135
+ case 'map': {
136
+ if (!letter || !unc_path) return { content: [{ type: 'text', text: 'map requires letter and unc_path.' }], isError: true };
137
+ const ps = `net use ${letter} "${unc_path}" /persistent:yes 2>&1`;
138
+ const result = await runPowerShell(ps, { timeout: 15000 });
139
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
140
+ }
141
+ case 'unmap': {
142
+ if (!letter) return { content: [{ type: 'text', text: 'unmap requires letter.' }], isError: true };
143
+ const ps = `net use ${letter} /delete /yes 2>&1`;
144
+ const result = await runPowerShell(ps, { timeout: 10000 });
145
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
146
+ }
147
+ }
148
+ },
149
+ );
150
+
151
+ server.tool(
152
+ 'windows_dns_cache',
153
+ 'View, filter, or clear the DNS resolver cache. Also resolves domain names.',
154
+ {
155
+ action: z.enum(['list', 'clear', 'resolve']).default('list').describe('Action'),
156
+ filter: z.string().optional().describe('Filter by domain (for list)'),
157
+ domain: z.string().optional().describe('Domain to resolve (for resolve)'),
158
+ limit: z.number().default(30).describe('Max entries (for list)'),
159
+ },
160
+ async ({ action, filter, domain, limit }) => {
161
+ switch (action) {
162
+ case 'list': {
163
+ const filterClause = filter ? `| Where-Object { $_.Entry -like '*${filter.replace(/'/g, "''")}*' }` : '';
164
+ const ps = `Get-DnsClientCache ${filterClause} -ErrorAction SilentlyContinue | Select-Object -First ${limit} Entry,RecordName,RecordType,Data,TimeToLive | ConvertTo-Json -Depth 3 -Compress`;
165
+ const result = await runPowerShell(ps, { timeout: 10000 });
166
+ if (!result.stdout) return { content: [{ type: 'text', text: 'DNS cache is empty.' }] };
167
+ const entries = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
168
+ const lines = entries.map((e: { Entry: string; RecordType: number; Data: string; TimeToLive: number }) =>
169
+ ` ${(e.Entry || '').padEnd(40).slice(0, 40)} ${String(e.RecordType).padEnd(5)} ${String(e.TimeToLive).padStart(6)}s ${e.Data || ''}`);
170
+ return { content: [{ type: 'text', text: `DNS Cache (${entries.length} entries):\n${lines.join('\n')}` }] };
171
+ }
172
+ case 'clear': {
173
+ const ps = `Clear-DnsClientCache; "DNS cache cleared"`;
174
+ const result = await runPowerShell(ps, { timeout: 5000 });
175
+ return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
176
+ }
177
+ case 'resolve': {
178
+ if (!domain) return { content: [{ type: 'text', text: 'resolve requires domain.' }], isError: true };
179
+ const ps = `Resolve-DnsName '${domain.replace(/'/g, "''")}' -ErrorAction Stop | Select-Object Name,Type,IPAddress,NameHost | ConvertTo-Json -Depth 3 -Compress`;
180
+ const result = await runPowerShell(ps, { timeout: 10000 });
181
+ if (result.exitCode !== 0) return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
182
+ const records = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
183
+ const lines = records.map((r: { Name: string; Type: number; IPAddress: string; NameHost: string }) =>
184
+ ` ${r.Name} ${r.IPAddress || r.NameHost || ''}`);
185
+ return { content: [{ type: 'text', text: `${domain}:\n${lines.join('\n')}` }] };
186
+ }
187
+ }
188
+ },
189
+ );
190
+ }
@@ -0,0 +1,117 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_terminal_session — Persistent interactive terminals (#35)
5
+ */
6
+
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { z } from 'zod';
9
+ import {
10
+ startSession,
11
+ sendToSession,
12
+ readSessionOutput,
13
+ listSessions,
14
+ terminateSession,
15
+ } from '../shell.js';
16
+
17
+ export function registerTerminalTools(server: McpServer): void {
18
+ server.tool(
19
+ 'windows_terminal_start',
20
+ 'Start a persistent interactive terminal session (PowerShell, cmd, bash, python, node, wsl).',
21
+ {
22
+ shell: z.enum(['pwsh', 'cmd', 'bash', 'python', 'node', 'wsl']).default('pwsh').describe('Shell type'),
23
+ },
24
+ async ({ shell }) => {
25
+ const session = startSession(shell);
26
+ // Give the shell a moment to start and produce initial output
27
+ await new Promise(resolve => setTimeout(resolve, 500));
28
+ return {
29
+ content: [{
30
+ type: 'text',
31
+ text: `Session started: PID ${session.pid} (${session.shell})\nUse windows_terminal_send to send commands, windows_terminal_read to read output.`,
32
+ }],
33
+ };
34
+ },
35
+ );
36
+
37
+ server.tool(
38
+ 'windows_terminal_send',
39
+ 'Send input to a running interactive terminal session.',
40
+ {
41
+ pid: z.number().describe('Session PID'),
42
+ input: z.string().describe('Text to send to the session'),
43
+ },
44
+ async ({ pid, input }) => {
45
+ try {
46
+ sendToSession(pid, input);
47
+ // Wait for output to arrive
48
+ await new Promise(resolve => setTimeout(resolve, 1000));
49
+ const lines = readSessionOutput(pid, -30);
50
+ return {
51
+ content: [{
52
+ type: 'text',
53
+ text: lines.join('\n') || '(no output yet)',
54
+ }],
55
+ };
56
+ } catch (err) {
57
+ return { content: [{ type: 'text', text: `Error: ${err}` }], isError: true };
58
+ }
59
+ },
60
+ );
61
+
62
+ server.tool(
63
+ 'windows_terminal_read',
64
+ 'Read output from a terminal session with pagination. Use negative offset to read from the end.',
65
+ {
66
+ pid: z.number().describe('Session PID'),
67
+ offset: z.number().default(0).describe('Line offset (negative = from end)'),
68
+ length: z.number().optional().describe('Number of lines to return'),
69
+ },
70
+ async ({ pid, offset, length }) => {
71
+ try {
72
+ const lines = readSessionOutput(pid, offset, length);
73
+ return {
74
+ content: [{
75
+ type: 'text',
76
+ text: lines.join('\n') || '(no output)',
77
+ }],
78
+ };
79
+ } catch (err) {
80
+ return { content: [{ type: 'text', text: `Error: ${err}` }], isError: true };
81
+ }
82
+ },
83
+ );
84
+
85
+ server.tool(
86
+ 'windows_terminal_list',
87
+ 'List all active terminal sessions.',
88
+ {},
89
+ async () => {
90
+ const sessions = listSessions();
91
+ if (sessions.length === 0) {
92
+ return { content: [{ type: 'text', text: 'No active sessions.' }] };
93
+ }
94
+ const lines = sessions.map(s =>
95
+ `PID ${s.pid} — ${s.shell} — ${s.running ? 'running' : 'ended'} — ${s.lines} lines — started ${s.startedAt}`,
96
+ );
97
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
98
+ },
99
+ );
100
+
101
+ server.tool(
102
+ 'windows_terminal_kill',
103
+ 'Terminate a terminal session by PID.',
104
+ {
105
+ pid: z.number().describe('Session PID to terminate'),
106
+ },
107
+ async ({ pid }) => {
108
+ const killed = terminateSession(pid);
109
+ return {
110
+ content: [{
111
+ type: 'text',
112
+ text: killed ? `Session ${pid} terminated.` : `No session with PID ${pid}.`,
113
+ }],
114
+ };
115
+ },
116
+ );
117
+ }