@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,230 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_file_read (#36), windows_file_write (#37),
5
+ * windows_file_edit (#38), windows_search (#39)
6
+ */
7
+ import { z } from 'zod';
8
+ import { readFile, writeFile, stat, mkdir } from 'node:fs/promises';
9
+ import { dirname, resolve, extname } from 'node:path';
10
+ import { runPowerShell } from '../shell.js';
11
+ export function registerFilesystemTools(server) {
12
+ // ── windows_file_read ────────────────────────────────────────────────
13
+ server.tool('windows_file_read', 'Read a file with line pagination. Supports text, images (base64), and binary detection. Use negative offset for tail behavior.', {
14
+ path: z.string().describe('Absolute file path'),
15
+ offset: z.number().default(0).describe('Line offset (0-based, negative = from end)'),
16
+ length: z.number().default(200).describe('Number of lines to return'),
17
+ }, async ({ path: filePath, offset, length }) => {
18
+ try {
19
+ const absPath = resolve(filePath);
20
+ const ext = extname(absPath).toLowerCase();
21
+ // Image files → base64
22
+ if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.svg'].includes(ext)) {
23
+ const data = await readFile(absPath);
24
+ const mimeMap = {
25
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
26
+ '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp',
27
+ '.ico': 'image/x-icon', '.svg': 'image/svg+xml',
28
+ };
29
+ return {
30
+ content: [{
31
+ type: 'image',
32
+ data: data.toString('base64'),
33
+ mimeType: mimeMap[ext] || 'application/octet-stream',
34
+ }],
35
+ };
36
+ }
37
+ // Text files → line pagination
38
+ const content = await readFile(absPath, 'utf-8');
39
+ const allLines = content.split('\n');
40
+ const total = allLines.length;
41
+ let start;
42
+ if (offset < 0) {
43
+ start = Math.max(0, total + offset);
44
+ }
45
+ else {
46
+ start = Math.min(offset, total);
47
+ }
48
+ const end = Math.min(start + length, total);
49
+ const lines = allLines.slice(start, end);
50
+ const numbered = lines.map((line, i) => `${String(start + i + 1).padStart(5)} ${line}`);
51
+ const header = `${absPath} — lines ${start + 1}-${end} of ${total}`;
52
+ return {
53
+ content: [{ type: 'text', text: `${header}\n${'─'.repeat(60)}\n${numbered.join('\n')}` }],
54
+ };
55
+ }
56
+ catch (err) {
57
+ return { content: [{ type: 'text', text: `Error reading file: ${err}` }], isError: true };
58
+ }
59
+ });
60
+ // ── windows_file_write ───────────────────────────────────────────────
61
+ server.tool('windows_file_write', 'Write or append to a text file. Creates parent directories if needed.', {
62
+ path: z.string().describe('Absolute file path'),
63
+ content: z.string().describe('Content to write'),
64
+ mode: z.enum(['write', 'append']).default('write').describe('Write mode'),
65
+ }, async ({ path: filePath, content, mode }) => {
66
+ try {
67
+ const absPath = resolve(filePath);
68
+ await mkdir(dirname(absPath), { recursive: true });
69
+ if (mode === 'append') {
70
+ const existing = await readFile(absPath, 'utf-8').catch(() => '');
71
+ await writeFile(absPath, existing + content, 'utf-8');
72
+ }
73
+ else {
74
+ await writeFile(absPath, content, 'utf-8');
75
+ }
76
+ const info = await stat(absPath);
77
+ return {
78
+ content: [{
79
+ type: 'text',
80
+ text: `Written: ${absPath} (${info.size} bytes, mode: ${mode})`,
81
+ }],
82
+ };
83
+ }
84
+ catch (err) {
85
+ return { content: [{ type: 'text', text: `Error writing file: ${err}` }], isError: true };
86
+ }
87
+ });
88
+ // ── windows_file_edit ────────────────────────────────────────────────
89
+ server.tool('windows_file_edit', 'Surgical file edit with find/replace. Validates expected replacement count.', {
90
+ path: z.string().describe('Absolute file path'),
91
+ old_string: z.string().describe('Text to find'),
92
+ new_string: z.string().describe('Replacement text'),
93
+ expected_count: z.number().optional().describe('Expected number of replacements (fails if mismatch)'),
94
+ replace_all: z.boolean().default(false).describe('Replace all occurrences'),
95
+ }, async ({ path: filePath, old_string, new_string, expected_count, replace_all }) => {
96
+ try {
97
+ const absPath = resolve(filePath);
98
+ const content = await readFile(absPath, 'utf-8');
99
+ // Count occurrences
100
+ let count = 0;
101
+ let idx = 0;
102
+ while ((idx = content.indexOf(old_string, idx)) !== -1) {
103
+ count++;
104
+ idx += old_string.length;
105
+ }
106
+ if (count === 0) {
107
+ // Try to find near-matches for helpful error
108
+ const lines = content.split('\n');
109
+ const needle = old_string.trim().split('\n')[0].trim();
110
+ const nearMatches = lines
111
+ .map((line, i) => ({ line: line.trim(), num: i + 1 }))
112
+ .filter(({ line }) => {
113
+ if (!needle)
114
+ return false;
115
+ // Simple similarity: shared words
116
+ const words = needle.toLowerCase().split(/\s+/);
117
+ const lineWords = line.toLowerCase().split(/\s+/);
118
+ const shared = words.filter(w => lineWords.includes(w)).length;
119
+ return shared >= Math.ceil(words.length * 0.5);
120
+ })
121
+ .slice(0, 3);
122
+ let msg = `No matches found for the specified text.`;
123
+ if (nearMatches.length > 0) {
124
+ msg += `\n\nNear matches:\n${nearMatches.map(m => ` Line ${m.num}: ${m.line}`).join('\n')}`;
125
+ }
126
+ return { content: [{ type: 'text', text: msg }], isError: true };
127
+ }
128
+ if (expected_count !== undefined && count !== expected_count) {
129
+ return {
130
+ content: [{
131
+ type: 'text',
132
+ text: `Expected ${expected_count} occurrence(s) but found ${count}. No changes made.`,
133
+ }],
134
+ isError: true,
135
+ };
136
+ }
137
+ let result;
138
+ if (replace_all) {
139
+ result = content.split(old_string).join(new_string);
140
+ }
141
+ else {
142
+ const pos = content.indexOf(old_string);
143
+ result = content.slice(0, pos) + new_string + content.slice(pos + old_string.length);
144
+ }
145
+ await writeFile(absPath, result, 'utf-8');
146
+ const replaced = replace_all ? count : 1;
147
+ return {
148
+ content: [{
149
+ type: 'text',
150
+ text: `Replaced ${replaced} occurrence(s) in ${absPath}`,
151
+ }],
152
+ };
153
+ }
154
+ catch (err) {
155
+ return { content: [{ type: 'text', text: `Error editing file: ${err}` }], isError: true };
156
+ }
157
+ });
158
+ // ── windows_search ───────────────────────────────────────────────────
159
+ server.tool('windows_search', 'Search for files by name pattern or search file contents. Uses ripgrep if available, falls back to PowerShell.', {
160
+ type: z.enum(['files', 'content']).describe('Search type: "files" for filename, "content" for inside files'),
161
+ pattern: z.string().describe('Search pattern (glob for files, regex or literal for content)'),
162
+ path: z.string().default('.').describe('Directory to search in'),
163
+ case_sensitive: z.boolean().default(false).describe('Case-sensitive search'),
164
+ file_pattern: z.string().optional().describe('Filter files by pattern (e.g. "*.ts") — only for content search'),
165
+ context_lines: z.number().default(0).describe('Lines of context around content matches'),
166
+ limit: z.number().default(50).describe('Max results'),
167
+ }, async ({ type, pattern, path: searchPath, case_sensitive, file_pattern, context_lines, limit }) => {
168
+ const absPath = resolve(searchPath);
169
+ if (type === 'files') {
170
+ const caseSense = case_sensitive ? '' : '-i';
171
+ const ps = `
172
+ Get-ChildItem -Path '${absPath.replace(/'/g, "''")}' -Recurse -File -ErrorAction SilentlyContinue |
173
+ Where-Object { $_.Name ${caseSense ? '-cmatch' : '-match'} '${pattern.replace(/'/g, "''")}' } |
174
+ Select-Object -First ${limit} |
175
+ ForEach-Object {
176
+ "$($_.Length.ToString().PadLeft(10)) $($_.LastWriteTime.ToString('yyyy-MM-dd HH:mm')) $($_.FullName)"
177
+ }`;
178
+ const result = await runPowerShell(ps, { timeout: 30000 });
179
+ if (result.exitCode !== 0) {
180
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
181
+ }
182
+ const lines = result.stdout ? result.stdout.split('\n') : [];
183
+ return {
184
+ content: [{
185
+ type: 'text',
186
+ text: lines.length > 0
187
+ ? `${lines.length} file(s) found:\n\n${' Size Modified Path'}\n${lines.join('\n')}`
188
+ : 'No files found.',
189
+ }],
190
+ };
191
+ }
192
+ // Content search — try ripgrep first, fall back to PowerShell
193
+ const caseFlag = case_sensitive ? '' : '-i';
194
+ const contextFlag = context_lines > 0 ? `-C ${context_lines}` : '';
195
+ const fileFlag = file_pattern ? `--glob '${file_pattern}'` : '';
196
+ // Try rg first
197
+ const rgCmd = `rg ${caseFlag} ${contextFlag} ${fileFlag} --max-count 5 -n '${pattern.replace(/'/g, "\\'")}' '${absPath.replace(/'/g, "\\'")}'`;
198
+ const rgResult = await runPowerShell(`& { ${rgCmd} } 2>$null | Select-Object -First ${limit * 3}`, { timeout: 30000 });
199
+ if (rgResult.exitCode === 0 && rgResult.stdout) {
200
+ const lines = rgResult.stdout.split('\n');
201
+ return {
202
+ content: [{
203
+ type: 'text',
204
+ text: `Content search results (ripgrep):\n\n${lines.join('\n')}`,
205
+ }],
206
+ };
207
+ }
208
+ // Fallback: PowerShell Select-String
209
+ const fileFilter = file_pattern ? `-Include '${file_pattern}'` : '';
210
+ const ps = `
211
+ Get-ChildItem -Path '${absPath.replace(/'/g, "''")}' -Recurse -File ${fileFilter} -ErrorAction SilentlyContinue |
212
+ Select-String -Pattern '${pattern.replace(/'/g, "''")}' ${case_sensitive ? '-CaseSensitive' : ''} -Context ${context_lines} |
213
+ Select-Object -First ${limit} |
214
+ ForEach-Object { "$($_.Path):$($_.LineNumber): $($_.Line.Trim())" }`;
215
+ const result = await runPowerShell(ps, { timeout: 60000 });
216
+ if (result.exitCode !== 0) {
217
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
218
+ }
219
+ const lines = result.stdout ? result.stdout.split('\n') : [];
220
+ return {
221
+ content: [{
222
+ type: 'text',
223
+ text: lines.length > 0
224
+ ? `${lines.length} match(es) found:\n\n${lines.join('\n')}`
225
+ : 'No matches found.',
226
+ }],
227
+ };
228
+ });
229
+ }
230
+ //# sourceMappingURL=filesystem.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerFirewallTools(server: McpServer): void;
3
+ //# sourceMappingURL=firewall.d.ts.map
@@ -0,0 +1,108 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tools: windows_firewall_get (#57), windows_firewall_manage (#58)
5
+ */
6
+ import { z } from 'zod';
7
+ import { runPowerShell } from '../shell.js';
8
+ export function registerFirewallTools(server) {
9
+ server.tool('windows_firewall_get', 'Get Windows Firewall status and list rules.', {
10
+ filter: z.string().optional().describe('Filter rules by name (substring)'),
11
+ direction: z.enum(['inbound', 'outbound', 'all']).default('all').describe('Rule direction'),
12
+ enabled_only: z.boolean().default(true).describe('Only show enabled rules'),
13
+ limit: z.number().default(30).describe('Max rules to return'),
14
+ }, async ({ filter, direction, enabled_only, limit }) => {
15
+ const dirFilter = direction === 'inbound' ? "| Where-Object { \\$_.Direction -eq 'Inbound' }"
16
+ : direction === 'outbound' ? "| Where-Object { \\$_.Direction -eq 'Outbound' }" : '';
17
+ const nameFilter = filter ? `| Where-Object { \\$_.DisplayName -like '*${filter.replace(/'/g, "''")}*' }` : '';
18
+ const enabledFilter = enabled_only ? "| Where-Object { \\$_.Enabled -eq 'True' }" : '';
19
+ const ps = `
20
+ $profiles = Get-NetFirewallProfile -ErrorAction SilentlyContinue | ForEach-Object {
21
+ [PSCustomObject]@{ Name = $_.Name; Enabled = $_.Enabled; DefaultInbound = $_.DefaultInboundAction; DefaultOutbound = $_.DefaultOutboundAction }
22
+ }
23
+
24
+ $rules = Get-NetFirewallRule ${dirFilter} ${nameFilter} ${enabledFilter} -ErrorAction SilentlyContinue |
25
+ Select-Object -First ${limit} | ForEach-Object {
26
+ $port = ($_ | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)
27
+ [PSCustomObject]@{
28
+ Name = $_.DisplayName
29
+ Direction = $_.Direction.ToString()
30
+ Action = $_.Action.ToString()
31
+ Enabled = $_.Enabled.ToString()
32
+ Protocol = if ($port) { $port.Protocol } else { 'Any' }
33
+ Port = if ($port.LocalPort) { $port.LocalPort -join ',' } else { 'Any' }
34
+ Program = ($_ | Get-NetFirewallApplicationFilter -ErrorAction SilentlyContinue).Program
35
+ }
36
+ }
37
+
38
+ [PSCustomObject]@{
39
+ Profiles = $profiles
40
+ Rules = $rules
41
+ } | ConvertTo-Json -Depth 4 -Compress`;
42
+ const result = await runPowerShell(ps, { timeout: 30000 });
43
+ if (result.exitCode !== 0) {
44
+ return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
45
+ }
46
+ const data = JSON.parse(result.stdout);
47
+ const lines = ['Firewall Profiles:'];
48
+ const profiles = Array.isArray(data.Profiles) ? data.Profiles : [data.Profiles];
49
+ for (const p of profiles.filter(Boolean)) {
50
+ lines.push(` ${p.Name}: ${p.Enabled ? 'ON' : 'OFF'} (In: ${p.DefaultInbound}, Out: ${p.DefaultOutbound})`);
51
+ }
52
+ const rules = Array.isArray(data.Rules) ? data.Rules : data.Rules ? [data.Rules] : [];
53
+ if (rules.length > 0) {
54
+ lines.push('', `Rules (${rules.length}):`);
55
+ for (const r of rules) {
56
+ const dir = r.Direction === 'Inbound' ? 'IN ' : 'OUT';
57
+ const act = r.Action === 'Allow' ? 'ALLOW' : 'BLOCK';
58
+ lines.push(` [${dir}] [${act}] ${(r.Name || '').padEnd(35).slice(0, 35)} ${r.Protocol}/${r.Port}`);
59
+ }
60
+ }
61
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
62
+ });
63
+ server.tool('windows_firewall_manage', 'Create, enable, disable, or delete a firewall rule.', {
64
+ action: z.enum(['create', 'enable', 'disable', 'delete']).describe('Action'),
65
+ name: z.string().describe('Rule name'),
66
+ direction: z.enum(['inbound', 'outbound']).optional().describe('Direction (for create)'),
67
+ rule_action: z.enum(['allow', 'block']).optional().describe('Allow or block (for create)'),
68
+ port: z.string().optional().describe('Port number or range (for create)'),
69
+ protocol: z.enum(['TCP', 'UDP', 'Any']).optional().describe('Protocol (for create)'),
70
+ program: z.string().optional().describe('Program path (for create)'),
71
+ }, async ({ action, name, direction, rule_action, port, protocol, program }) => {
72
+ let ps;
73
+ switch (action) {
74
+ case 'create': {
75
+ if (!direction || !rule_action) {
76
+ return { content: [{ type: 'text', text: 'Create requires direction and rule_action.' }], isError: true };
77
+ }
78
+ const dir = direction === 'inbound' ? 'Inbound' : 'Outbound';
79
+ const act = rule_action === 'allow' ? 'Allow' : 'Block';
80
+ const parts = [`New-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -Direction ${dir} -Action ${act}`];
81
+ if (port)
82
+ parts.push(`-LocalPort ${port}`);
83
+ if (protocol && protocol !== 'Any')
84
+ parts.push(`-Protocol ${protocol}`);
85
+ if (program)
86
+ parts.push(`-Program '${program.replace(/'/g, "''")}'`);
87
+ parts.push('-ErrorAction Stop');
88
+ ps = `${parts.join(' ')} | Select-Object DisplayName,Direction,Action,Enabled | ConvertTo-Json -Compress`;
89
+ break;
90
+ }
91
+ case 'enable':
92
+ ps = `Enable-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Enabled: ${name}"`;
93
+ break;
94
+ case 'disable':
95
+ ps = `Disable-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Disabled: ${name}"`;
96
+ break;
97
+ case 'delete':
98
+ ps = `Remove-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Deleted: ${name}"`;
99
+ break;
100
+ }
101
+ const result = await runPowerShell(ps, { timeout: 15000 });
102
+ return {
103
+ content: [{ type: 'text', text: result.stdout || result.stderr }],
104
+ isError: result.exitCode !== 0,
105
+ };
106
+ });
107
+ }
108
+ //# sourceMappingURL=firewall.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerHostsTools(server: McpServer): void;
3
+ //# sourceMappingURL=hosts.d.ts.map
@@ -0,0 +1,119 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ * SPDX-License-Identifier: GPL-3.0-or-later
3
+ *
4
+ * Tool: windows_hosts_file (#51)
5
+ */
6
+ import { z } from 'zod';
7
+ import { readFile, writeFile, copyFile } from 'node:fs/promises';
8
+ const HOSTS_PATH = 'C:\\Windows\\System32\\drivers\\etc\\hosts';
9
+ export function registerHostsTools(server) {
10
+ server.tool('windows_hosts_file', 'Read and manage the Windows hosts file. List, add, remove, or toggle entries.', {
11
+ action: z.enum(['list', 'add', 'remove', 'enable', 'disable']).default('list').describe('Action'),
12
+ ip: z.string().optional().describe('IP address (for add)'),
13
+ hostname: z.string().optional().describe('Hostname (for add/remove/enable/disable)'),
14
+ comment: z.string().optional().describe('Comment (for add)'),
15
+ }, async ({ action, ip, hostname, comment }) => {
16
+ try {
17
+ const content = await readFile(HOSTS_PATH, 'utf-8');
18
+ const lines = content.split(/\r?\n/);
19
+ if (action === 'list') {
20
+ const entries = [];
21
+ lines.forEach((line, i) => {
22
+ const trimmed = line.trim();
23
+ if (!trimmed || trimmed.startsWith('#') && !trimmed.match(/^#\s*\d/)) {
24
+ // Check if it's a commented-out entry
25
+ const commented = trimmed.replace(/^#\s*/, '');
26
+ const parts = commented.split(/\s+/);
27
+ if (parts.length >= 2 && parts[0].match(/^\d+\.\d+\.\d+\.\d+$|^[a-f0-9:]+$/i)) {
28
+ entries.push({
29
+ line: i + 1,
30
+ enabled: false,
31
+ ip: parts[0],
32
+ host: parts[1],
33
+ comment: parts.slice(2).join(' ').replace(/^#\s*/, ''),
34
+ });
35
+ }
36
+ return;
37
+ }
38
+ const parts = trimmed.split(/\s+/);
39
+ if (parts.length >= 2 && parts[0].match(/^\d+\.\d+\.\d+\.\d+$|^[a-f0-9:]+$/i)) {
40
+ entries.push({
41
+ line: i + 1,
42
+ enabled: true,
43
+ ip: parts[0],
44
+ host: parts[1],
45
+ comment: parts.slice(2).join(' ').replace(/^#\s*/, ''),
46
+ });
47
+ }
48
+ });
49
+ const output = entries.map(e => {
50
+ const status = e.enabled ? '[ON] ' : '[OFF]';
51
+ return `${status} ${e.ip.padEnd(18)} ${e.host.padEnd(35)} ${e.comment}`;
52
+ });
53
+ const header = `State ${'IP'.padEnd(18)} ${'Hostname'.padEnd(35)} Comment`;
54
+ return {
55
+ content: [{ type: 'text', text: `${HOSTS_PATH}\n${header}\n${'─'.repeat(80)}\n${output.join('\n')}\n\n${entries.length} entries` }],
56
+ };
57
+ }
58
+ // Backup before modification
59
+ await copyFile(HOSTS_PATH, HOSTS_PATH + '.bak');
60
+ if (action === 'add') {
61
+ if (!ip || !hostname) {
62
+ return { content: [{ type: 'text', text: 'Add requires ip and hostname.' }], isError: true };
63
+ }
64
+ const entry = comment ? `${ip}\t${hostname}\t# ${comment}` : `${ip}\t${hostname}`;
65
+ const newContent = content.trimEnd() + '\n' + entry + '\n';
66
+ await writeFile(HOSTS_PATH, newContent, 'utf-8');
67
+ return { content: [{ type: 'text', text: `Added: ${ip} ${hostname}` }] };
68
+ }
69
+ if (action === 'remove') {
70
+ if (!hostname) {
71
+ return { content: [{ type: 'text', text: 'Remove requires hostname.' }], isError: true };
72
+ }
73
+ const filtered = lines.filter(line => {
74
+ const parts = line.trim().replace(/^#\s*/, '').split(/\s+/);
75
+ return !(parts.length >= 2 && parts[1] === hostname);
76
+ });
77
+ await writeFile(HOSTS_PATH, filtered.join('\n'), 'utf-8');
78
+ return { content: [{ type: 'text', text: `Removed entries for: ${hostname}` }] };
79
+ }
80
+ if (action === 'disable') {
81
+ if (!hostname) {
82
+ return { content: [{ type: 'text', text: 'Disable requires hostname.' }], isError: true };
83
+ }
84
+ const updated = lines.map(line => {
85
+ const parts = line.trim().split(/\s+/);
86
+ if (parts.length >= 2 && parts[1] === hostname && !line.trim().startsWith('#')) {
87
+ return '# ' + line;
88
+ }
89
+ return line;
90
+ });
91
+ await writeFile(HOSTS_PATH, updated.join('\n'), 'utf-8');
92
+ return { content: [{ type: 'text', text: `Disabled: ${hostname}` }] };
93
+ }
94
+ if (action === 'enable') {
95
+ if (!hostname) {
96
+ return { content: [{ type: 'text', text: 'Enable requires hostname.' }], isError: true };
97
+ }
98
+ const updated = lines.map(line => {
99
+ const trimmed = line.trim();
100
+ if (trimmed.startsWith('#')) {
101
+ const uncommented = trimmed.replace(/^#\s*/, '');
102
+ const parts = uncommented.split(/\s+/);
103
+ if (parts.length >= 2 && parts[1] === hostname) {
104
+ return uncommented;
105
+ }
106
+ }
107
+ return line;
108
+ });
109
+ await writeFile(HOSTS_PATH, updated.join('\n'), 'utf-8');
110
+ return { content: [{ type: 'text', text: `Enabled: ${hostname}` }] };
111
+ }
112
+ return { content: [{ type: 'text', text: 'Unknown action.' }], isError: true };
113
+ }
114
+ catch (err) {
115
+ return { content: [{ type: 'text', text: `Error: ${err}. Hosts file modification may require elevation.` }], isError: true };
116
+ }
117
+ });
118
+ }
119
+ //# sourceMappingURL=hosts.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerMaintenanceTools(server: McpServer): void;
3
+ //# sourceMappingURL=maintenance.d.ts.map