@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.
- package/.gitattributes +94 -0
- package/.gitmessage +9 -0
- package/.mokogitea/ISSUE_TEMPLATE/adr.md +110 -0
- package/.mokogitea/ISSUE_TEMPLATE/bug_report.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/config.yml +18 -0
- package/.mokogitea/ISSUE_TEMPLATE/documentation.md +52 -0
- package/.mokogitea/ISSUE_TEMPLATE/feature_request.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_connection_issue.md +67 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_tool_request.md +49 -0
- package/.mokogitea/ISSUE_TEMPLATE/question.md +82 -0
- package/.mokogitea/ISSUE_TEMPLATE/rfc.md +126 -0
- package/.mokogitea/ISSUE_TEMPLATE/security.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/version.md +24 -0
- package/.mokogitea/branch-protection.yml +251 -0
- package/.mokogitea/workflows/auto-assign.yml +76 -0
- package/.mokogitea/workflows/auto-bump.yml +66 -0
- package/.mokogitea/workflows/auto-dev-issue.yml +207 -0
- package/.mokogitea/workflows/auto-release.yml +421 -0
- package/.mokogitea/workflows/branch-cleanup.yml +48 -0
- package/.mokogitea/workflows/cascade-dev.yml +10 -0
- package/.mokogitea/workflows/changelog-validation.yml +101 -0
- package/.mokogitea/workflows/ci-generic.yml +191 -0
- package/.mokogitea/workflows/cleanup.yml +87 -0
- package/.mokogitea/workflows/codeql-analysis.yml +115 -0
- package/.mokogitea/workflows/copilot-agent.yml +44 -0
- package/.mokogitea/workflows/deploy-manual.yml +126 -0
- package/.mokogitea/workflows/enterprise-firewall-setup.yml +758 -0
- package/.mokogitea/workflows/gitleaks.yml +92 -0
- package/.mokogitea/workflows/issue-branch.yml +73 -0
- package/.mokogitea/workflows/mcp-auto-release.yml +278 -0
- package/.mokogitea/workflows/mcp-build-test.yml +65 -0
- package/.mokogitea/workflows/mcp-sdk-check.yml +109 -0
- package/.mokogitea/workflows/mcp-tool-inventory.yml +61 -0
- package/.mokogitea/workflows/notify.yml +70 -0
- package/.mokogitea/workflows/npm-publish.yml +113 -0
- package/.mokogitea/workflows/pr-check.yml +534 -0
- package/.mokogitea/workflows/pre-release.yml +252 -0
- package/.mokogitea/workflows/rc-revert.yml +66 -0
- package/.mokogitea/workflows/repo-health.yml +712 -0
- package/.mokogitea/workflows/repository-cleanup.yml +525 -0
- package/.mokogitea/workflows/security-audit.yml +82 -0
- package/.mokogitea/workflows/standards-compliance.yml +2614 -0
- package/.mokogitea/workflows/sync-version-on-merge.yml +133 -0
- package/.mokogitea/workflows/update-server.yml +312 -0
- package/.mokogitea/workflows/workflow-sync-trigger.yml +73 -0
- package/CHANGELOG.md +130 -0
- package/CLAUDE.md +49 -0
- package/CONTRIBUTING.md +161 -0
- package/ISSUES.md +601 -0
- package/Makefile +70 -0
- package/README.md +80 -0
- package/automation/ci-issue-reporter.sh +237 -0
- package/config.example.json +18 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +111 -0
- package/dist/shell.d.ts +50 -0
- package/dist/shell.js +209 -0
- package/dist/tools/apps.d.ts +3 -0
- package/dist/tools/apps.js +63 -0
- package/dist/tools/audio.d.ts +3 -0
- package/dist/tools/audio.js +142 -0
- package/dist/tools/audio_apps.d.ts +3 -0
- package/dist/tools/audio_apps.js +86 -0
- package/dist/tools/automation.d.ts +3 -0
- package/dist/tools/automation.js +261 -0
- package/dist/tools/bluetooth.d.ts +3 -0
- package/dist/tools/bluetooth.js +96 -0
- package/dist/tools/clipboard.d.ts +3 -0
- package/dist/tools/clipboard.js +118 -0
- package/dist/tools/config.d.ts +3 -0
- package/dist/tools/config.js +85 -0
- package/dist/tools/dialog.d.ts +3 -0
- package/dist/tools/dialog.js +72 -0
- package/dist/tools/display.d.ts +3 -0
- package/dist/tools/display.js +256 -0
- package/dist/tools/drives.d.ts +3 -0
- package/dist/tools/drives.js +98 -0
- package/dist/tools/environment.d.ts +3 -0
- package/dist/tools/environment.js +129 -0
- package/dist/tools/execute.d.ts +3 -0
- package/dist/tools/execute.js +28 -0
- package/dist/tools/filesystem.d.ts +3 -0
- package/dist/tools/filesystem.js +230 -0
- package/dist/tools/firewall.d.ts +3 -0
- package/dist/tools/firewall.js +108 -0
- package/dist/tools/hosts.d.ts +3 -0
- package/dist/tools/hosts.js +119 -0
- package/dist/tools/maintenance.d.ts +3 -0
- package/dist/tools/maintenance.js +236 -0
- package/dist/tools/netstat.d.ts +3 -0
- package/dist/tools/netstat.js +56 -0
- package/dist/tools/network.d.ts +3 -0
- package/dist/tools/network.js +70 -0
- package/dist/tools/notification.d.ts +3 -0
- package/dist/tools/notification.js +41 -0
- package/dist/tools/power.d.ts +3 -0
- package/dist/tools/power.js +104 -0
- package/dist/tools/printer.d.ts +3 -0
- package/dist/tools/printer.js +97 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +54 -0
- package/dist/tools/process_kill.d.ts +3 -0
- package/dist/tools/process_kill.js +48 -0
- package/dist/tools/recycle_bin.d.ts +3 -0
- package/dist/tools/recycle_bin.js +108 -0
- package/dist/tools/registry.d.ts +3 -0
- package/dist/tools/registry.js +136 -0
- package/dist/tools/scheduler.d.ts +3 -0
- package/dist/tools/scheduler.js +116 -0
- package/dist/tools/service.d.ts +3 -0
- package/dist/tools/service.js +79 -0
- package/dist/tools/startup.d.ts +3 -0
- package/dist/tools/startup.js +159 -0
- package/dist/tools/storage.d.ts +3 -0
- package/dist/tools/storage.js +129 -0
- package/dist/tools/system.d.ts +3 -0
- package/dist/tools/system.js +84 -0
- package/dist/tools/system_mgmt.d.ts +3 -0
- package/dist/tools/system_mgmt.js +174 -0
- package/dist/tools/terminal.d.ts +3 -0
- package/dist/tools/terminal.js +80 -0
- package/dist/tools/theme.d.ts +3 -0
- package/dist/tools/theme.js +165 -0
- package/dist/tools/usb.d.ts +3 -0
- package/dist/tools/usb.js +52 -0
- package/dist/tools/virtual_desktop.d.ts +3 -0
- package/dist/tools/virtual_desktop.js +112 -0
- package/dist/tools/wifi.d.ts +3 -0
- package/dist/tools/wifi.js +136 -0
- package/dist/tools/window.d.ts +3 -0
- package/dist/tools/window.js +189 -0
- package/dist/tools/winget.d.ts +3 -0
- package/dist/tools/winget.js +79 -0
- package/dist/tools/wsl.d.ts +3 -0
- package/dist/tools/wsl.js +99 -0
- package/docs/API.md +63 -0
- package/docs/ARCHITECTURE.md +73 -0
- package/docs/INSTALLATION.md +102 -0
- package/docs/index.md +12 -0
- package/package.json +35 -0
- package/scripts/setup.mjs +123 -0
- package/src/index.ts +125 -0
- package/src/shell.ts +253 -0
- package/src/tools/apps.ts +76 -0
- package/src/tools/audio.ts +161 -0
- package/src/tools/audio_apps.ts +98 -0
- package/src/tools/automation.ts +297 -0
- package/src/tools/bluetooth.ts +114 -0
- package/src/tools/clipboard.ts +138 -0
- package/src/tools/config.ts +105 -0
- package/src/tools/dialog.ts +87 -0
- package/src/tools/display.ts +285 -0
- package/src/tools/drives.ts +124 -0
- package/src/tools/environment.ts +146 -0
- package/src/tools/execute.ts +35 -0
- package/src/tools/filesystem.ts +273 -0
- package/src/tools/firewall.ts +125 -0
- package/src/tools/hosts.ts +135 -0
- package/src/tools/maintenance.ts +299 -0
- package/src/tools/netstat.ts +72 -0
- package/src/tools/network.ts +84 -0
- package/src/tools/notification.ts +50 -0
- package/src/tools/power.ts +123 -0
- package/src/tools/printer.ts +114 -0
- package/src/tools/process.ts +80 -0
- package/src/tools/process_kill.ts +57 -0
- package/src/tools/recycle_bin.ts +126 -0
- package/src/tools/registry.ts +165 -0
- package/src/tools/scheduler.ts +140 -0
- package/src/tools/service.ts +102 -0
- package/src/tools/startup.ts +180 -0
- package/src/tools/storage.ts +141 -0
- package/src/tools/system.ts +99 -0
- package/src/tools/system_mgmt.ts +190 -0
- package/src/tools/terminal.ts +117 -0
- package/src/tools/theme.ts +205 -0
- package/src/tools/usb.ts +65 -0
- package/src/tools/virtual_desktop.ts +122 -0
- package/src/tools/wifi.ts +157 -0
- package/src/tools/window.ts +211 -0
- package/src/tools/winget.ts +100 -0
- package/src/tools/wsl.ts +112 -0
- 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,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,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
|