@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,125 @@
|
|
|
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
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { runPowerShell } from '../shell.js';
|
|
10
|
+
|
|
11
|
+
export function registerFirewallTools(server: McpServer): void {
|
|
12
|
+
server.tool(
|
|
13
|
+
'windows_firewall_get',
|
|
14
|
+
'Get Windows Firewall status and list rules.',
|
|
15
|
+
{
|
|
16
|
+
filter: z.string().optional().describe('Filter rules by name (substring)'),
|
|
17
|
+
direction: z.enum(['inbound', 'outbound', 'all']).default('all').describe('Rule direction'),
|
|
18
|
+
enabled_only: z.boolean().default(true).describe('Only show enabled rules'),
|
|
19
|
+
limit: z.number().default(30).describe('Max rules to return'),
|
|
20
|
+
},
|
|
21
|
+
async ({ filter, direction, enabled_only, limit }) => {
|
|
22
|
+
const dirFilter = direction === 'inbound' ? "| Where-Object { \\$_.Direction -eq 'Inbound' }"
|
|
23
|
+
: direction === 'outbound' ? "| Where-Object { \\$_.Direction -eq 'Outbound' }" : '';
|
|
24
|
+
const nameFilter = filter ? `| Where-Object { \\$_.DisplayName -like '*${filter.replace(/'/g, "''")}*' }` : '';
|
|
25
|
+
const enabledFilter = enabled_only ? "| Where-Object { \\$_.Enabled -eq 'True' }" : '';
|
|
26
|
+
|
|
27
|
+
const ps = `
|
|
28
|
+
$profiles = Get-NetFirewallProfile -ErrorAction SilentlyContinue | ForEach-Object {
|
|
29
|
+
[PSCustomObject]@{ Name = $_.Name; Enabled = $_.Enabled; DefaultInbound = $_.DefaultInboundAction; DefaultOutbound = $_.DefaultOutboundAction }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$rules = Get-NetFirewallRule ${dirFilter} ${nameFilter} ${enabledFilter} -ErrorAction SilentlyContinue |
|
|
33
|
+
Select-Object -First ${limit} | ForEach-Object {
|
|
34
|
+
$port = ($_ | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)
|
|
35
|
+
[PSCustomObject]@{
|
|
36
|
+
Name = $_.DisplayName
|
|
37
|
+
Direction = $_.Direction.ToString()
|
|
38
|
+
Action = $_.Action.ToString()
|
|
39
|
+
Enabled = $_.Enabled.ToString()
|
|
40
|
+
Protocol = if ($port) { $port.Protocol } else { 'Any' }
|
|
41
|
+
Port = if ($port.LocalPort) { $port.LocalPort -join ',' } else { 'Any' }
|
|
42
|
+
Program = ($_ | Get-NetFirewallApplicationFilter -ErrorAction SilentlyContinue).Program
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[PSCustomObject]@{
|
|
47
|
+
Profiles = $profiles
|
|
48
|
+
Rules = $rules
|
|
49
|
+
} | ConvertTo-Json -Depth 4 -Compress`;
|
|
50
|
+
|
|
51
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
52
|
+
if (result.exitCode !== 0) {
|
|
53
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = JSON.parse(result.stdout);
|
|
57
|
+
const lines: string[] = ['Firewall Profiles:'];
|
|
58
|
+
const profiles = Array.isArray(data.Profiles) ? data.Profiles : [data.Profiles];
|
|
59
|
+
for (const p of profiles.filter(Boolean)) {
|
|
60
|
+
lines.push(` ${p.Name}: ${p.Enabled ? 'ON' : 'OFF'} (In: ${p.DefaultInbound}, Out: ${p.DefaultOutbound})`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rules = Array.isArray(data.Rules) ? data.Rules : data.Rules ? [data.Rules] : [];
|
|
64
|
+
if (rules.length > 0) {
|
|
65
|
+
lines.push('', `Rules (${rules.length}):`);
|
|
66
|
+
for (const r of rules) {
|
|
67
|
+
const dir = r.Direction === 'Inbound' ? 'IN ' : 'OUT';
|
|
68
|
+
const act = r.Action === 'Allow' ? 'ALLOW' : 'BLOCK';
|
|
69
|
+
lines.push(` [${dir}] [${act}] ${(r.Name || '').padEnd(35).slice(0, 35)} ${r.Protocol}/${r.Port}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
server.tool(
|
|
78
|
+
'windows_firewall_manage',
|
|
79
|
+
'Create, enable, disable, or delete a firewall rule.',
|
|
80
|
+
{
|
|
81
|
+
action: z.enum(['create', 'enable', 'disable', 'delete']).describe('Action'),
|
|
82
|
+
name: z.string().describe('Rule name'),
|
|
83
|
+
direction: z.enum(['inbound', 'outbound']).optional().describe('Direction (for create)'),
|
|
84
|
+
rule_action: z.enum(['allow', 'block']).optional().describe('Allow or block (for create)'),
|
|
85
|
+
port: z.string().optional().describe('Port number or range (for create)'),
|
|
86
|
+
protocol: z.enum(['TCP', 'UDP', 'Any']).optional().describe('Protocol (for create)'),
|
|
87
|
+
program: z.string().optional().describe('Program path (for create)'),
|
|
88
|
+
},
|
|
89
|
+
async ({ action, name, direction, rule_action, port, protocol, program }) => {
|
|
90
|
+
let ps: string;
|
|
91
|
+
|
|
92
|
+
switch (action) {
|
|
93
|
+
case 'create': {
|
|
94
|
+
if (!direction || !rule_action) {
|
|
95
|
+
return { content: [{ type: 'text', text: 'Create requires direction and rule_action.' }], isError: true };
|
|
96
|
+
}
|
|
97
|
+
const dir = direction === 'inbound' ? 'Inbound' : 'Outbound';
|
|
98
|
+
const act = rule_action === 'allow' ? 'Allow' : 'Block';
|
|
99
|
+
const parts = [`New-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -Direction ${dir} -Action ${act}`];
|
|
100
|
+
if (port) parts.push(`-LocalPort ${port}`);
|
|
101
|
+
if (protocol && protocol !== 'Any') parts.push(`-Protocol ${protocol}`);
|
|
102
|
+
if (program) parts.push(`-Program '${program.replace(/'/g, "''")}'`);
|
|
103
|
+
parts.push('-ErrorAction Stop');
|
|
104
|
+
ps = `${parts.join(' ')} | Select-Object DisplayName,Direction,Action,Enabled | ConvertTo-Json -Compress`;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'enable':
|
|
108
|
+
ps = `Enable-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Enabled: ${name}"`;
|
|
109
|
+
break;
|
|
110
|
+
case 'disable':
|
|
111
|
+
ps = `Disable-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Disabled: ${name}"`;
|
|
112
|
+
break;
|
|
113
|
+
case 'delete':
|
|
114
|
+
ps = `Remove-NetFirewallRule -DisplayName '${name.replace(/'/g, "''")}' -ErrorAction Stop; "Deleted: ${name}"`;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: 'text', text: result.stdout || result.stderr }],
|
|
121
|
+
isError: result.exitCode !== 0,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { readFile, writeFile, copyFile } from 'node:fs/promises';
|
|
10
|
+
|
|
11
|
+
const HOSTS_PATH = 'C:\\Windows\\System32\\drivers\\etc\\hosts';
|
|
12
|
+
|
|
13
|
+
export function registerHostsTools(server: McpServer): void {
|
|
14
|
+
server.tool(
|
|
15
|
+
'windows_hosts_file',
|
|
16
|
+
'Read and manage the Windows hosts file. List, add, remove, or toggle entries.',
|
|
17
|
+
{
|
|
18
|
+
action: z.enum(['list', 'add', 'remove', 'enable', 'disable']).default('list').describe('Action'),
|
|
19
|
+
ip: z.string().optional().describe('IP address (for add)'),
|
|
20
|
+
hostname: z.string().optional().describe('Hostname (for add/remove/enable/disable)'),
|
|
21
|
+
comment: z.string().optional().describe('Comment (for add)'),
|
|
22
|
+
},
|
|
23
|
+
async ({ action, ip, hostname, comment }) => {
|
|
24
|
+
try {
|
|
25
|
+
const content = await readFile(HOSTS_PATH, 'utf-8');
|
|
26
|
+
const lines = content.split(/\r?\n/);
|
|
27
|
+
|
|
28
|
+
if (action === 'list') {
|
|
29
|
+
const entries: Array<{ line: number; enabled: boolean; ip: string; host: string; comment: string }> = [];
|
|
30
|
+
lines.forEach((line, i) => {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (!trimmed || trimmed.startsWith('#') && !trimmed.match(/^#\s*\d/)) {
|
|
33
|
+
// Check if it's a commented-out entry
|
|
34
|
+
const commented = trimmed.replace(/^#\s*/, '');
|
|
35
|
+
const parts = commented.split(/\s+/);
|
|
36
|
+
if (parts.length >= 2 && parts[0].match(/^\d+\.\d+\.\d+\.\d+$|^[a-f0-9:]+$/i)) {
|
|
37
|
+
entries.push({
|
|
38
|
+
line: i + 1,
|
|
39
|
+
enabled: false,
|
|
40
|
+
ip: parts[0],
|
|
41
|
+
host: parts[1],
|
|
42
|
+
comment: parts.slice(2).join(' ').replace(/^#\s*/, ''),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const parts = trimmed.split(/\s+/);
|
|
48
|
+
if (parts.length >= 2 && parts[0].match(/^\d+\.\d+\.\d+\.\d+$|^[a-f0-9:]+$/i)) {
|
|
49
|
+
entries.push({
|
|
50
|
+
line: i + 1,
|
|
51
|
+
enabled: true,
|
|
52
|
+
ip: parts[0],
|
|
53
|
+
host: parts[1],
|
|
54
|
+
comment: parts.slice(2).join(' ').replace(/^#\s*/, ''),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const output = entries.map(e => {
|
|
60
|
+
const status = e.enabled ? '[ON] ' : '[OFF]';
|
|
61
|
+
return `${status} ${e.ip.padEnd(18)} ${e.host.padEnd(35)} ${e.comment}`;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const header = `State ${'IP'.padEnd(18)} ${'Hostname'.padEnd(35)} Comment`;
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: 'text', text: `${HOSTS_PATH}\n${header}\n${'─'.repeat(80)}\n${output.join('\n')}\n\n${entries.length} entries` }],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Backup before modification
|
|
71
|
+
await copyFile(HOSTS_PATH, HOSTS_PATH + '.bak');
|
|
72
|
+
|
|
73
|
+
if (action === 'add') {
|
|
74
|
+
if (!ip || !hostname) {
|
|
75
|
+
return { content: [{ type: 'text', text: 'Add requires ip and hostname.' }], isError: true };
|
|
76
|
+
}
|
|
77
|
+
const entry = comment ? `${ip}\t${hostname}\t# ${comment}` : `${ip}\t${hostname}`;
|
|
78
|
+
const newContent = content.trimEnd() + '\n' + entry + '\n';
|
|
79
|
+
await writeFile(HOSTS_PATH, newContent, 'utf-8');
|
|
80
|
+
return { content: [{ type: 'text', text: `Added: ${ip} ${hostname}` }] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (action === 'remove') {
|
|
84
|
+
if (!hostname) {
|
|
85
|
+
return { content: [{ type: 'text', text: 'Remove requires hostname.' }], isError: true };
|
|
86
|
+
}
|
|
87
|
+
const filtered = lines.filter(line => {
|
|
88
|
+
const parts = line.trim().replace(/^#\s*/, '').split(/\s+/);
|
|
89
|
+
return !(parts.length >= 2 && parts[1] === hostname);
|
|
90
|
+
});
|
|
91
|
+
await writeFile(HOSTS_PATH, filtered.join('\n'), 'utf-8');
|
|
92
|
+
return { content: [{ type: 'text', text: `Removed entries for: ${hostname}` }] };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (action === 'disable') {
|
|
96
|
+
if (!hostname) {
|
|
97
|
+
return { content: [{ type: 'text', text: 'Disable requires hostname.' }], isError: true };
|
|
98
|
+
}
|
|
99
|
+
const updated = lines.map(line => {
|
|
100
|
+
const parts = line.trim().split(/\s+/);
|
|
101
|
+
if (parts.length >= 2 && parts[1] === hostname && !line.trim().startsWith('#')) {
|
|
102
|
+
return '# ' + line;
|
|
103
|
+
}
|
|
104
|
+
return line;
|
|
105
|
+
});
|
|
106
|
+
await writeFile(HOSTS_PATH, updated.join('\n'), 'utf-8');
|
|
107
|
+
return { content: [{ type: 'text', text: `Disabled: ${hostname}` }] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (action === 'enable') {
|
|
111
|
+
if (!hostname) {
|
|
112
|
+
return { content: [{ type: 'text', text: 'Enable requires hostname.' }], isError: true };
|
|
113
|
+
}
|
|
114
|
+
const updated = lines.map(line => {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (trimmed.startsWith('#')) {
|
|
117
|
+
const uncommented = trimmed.replace(/^#\s*/, '');
|
|
118
|
+
const parts = uncommented.split(/\s+/);
|
|
119
|
+
if (parts.length >= 2 && parts[1] === hostname) {
|
|
120
|
+
return uncommented;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return line;
|
|
124
|
+
});
|
|
125
|
+
await writeFile(HOSTS_PATH, updated.join('\n'), 'utf-8');
|
|
126
|
+
return { content: [{ type: 'text', text: `Enabled: ${hostname}` }] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { content: [{ type: 'text', text: 'Unknown action.' }], isError: true };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return { content: [{ type: 'text', text: `Error: ${err}. Hosts file modification may require elevation.` }], isError: true };
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_updates (#59), windows_event_log (#60),
|
|
5
|
+
* windows_restore_point (#61), windows_certificate_list (#62),
|
|
6
|
+
* windows_performance_monitor (#63)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { runPowerShell } from '../shell.js';
|
|
12
|
+
|
|
13
|
+
export function registerMaintenanceTools(server: McpServer): void {
|
|
14
|
+
|
|
15
|
+
server.tool(
|
|
16
|
+
'windows_updates',
|
|
17
|
+
'Check Windows Update status, pending updates, and recent history.',
|
|
18
|
+
{
|
|
19
|
+
action: z.enum(['status', 'history']).default('status').describe('Check status or view history'),
|
|
20
|
+
limit: z.number().default(20).describe('Max history entries'),
|
|
21
|
+
},
|
|
22
|
+
async ({ action, limit }) => {
|
|
23
|
+
if (action === 'history') {
|
|
24
|
+
const ps = `
|
|
25
|
+
$session = New-Object -ComObject Microsoft.Update.Session
|
|
26
|
+
$searcher = $session.CreateUpdateSearcher()
|
|
27
|
+
$count = $searcher.GetTotalHistoryCount()
|
|
28
|
+
$history = $searcher.QueryHistory(0, [math]::Min($count, ${limit}))
|
|
29
|
+
$history | ForEach-Object {
|
|
30
|
+
[PSCustomObject]@{
|
|
31
|
+
Date = $_.Date.ToString('yyyy-MM-dd HH:mm')
|
|
32
|
+
Title = $_.Title
|
|
33
|
+
Result = switch ($_.ResultCode) { 0 {'Not Started'} 1 {'In Progress'} 2 {'Succeeded'} 3 {'Succeeded with Errors'} 4 {'Failed'} 5 {'Aborted'} default {'Unknown'} }
|
|
34
|
+
}
|
|
35
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
36
|
+
|
|
37
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
38
|
+
if (result.exitCode !== 0) {
|
|
39
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
if (!result.stdout) {
|
|
42
|
+
return { content: [{ type: 'text', text: 'No update history.' }] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const entries = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
46
|
+
const lines = entries.map((e: { Date: string; Title: string; Result: string }) =>
|
|
47
|
+
`${e.Result.padEnd(10)} ${e.Date} ${(e.Title || '').slice(0, 70)}`,
|
|
48
|
+
);
|
|
49
|
+
return { content: [{ type: 'text', text: `Recent updates:\n${lines.join('\n')}` }] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Status
|
|
53
|
+
const ps = `
|
|
54
|
+
$reboot = Test-Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired'
|
|
55
|
+
$lastCheck = (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\Results\\Detect' -Name LastSuccessTime -ErrorAction SilentlyContinue).LastSuccessTime
|
|
56
|
+
[PSCustomObject]@{
|
|
57
|
+
RestartPending = $reboot
|
|
58
|
+
LastCheckTime = $lastCheck
|
|
59
|
+
} | ConvertTo-Json -Compress`;
|
|
60
|
+
|
|
61
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
62
|
+
if (result.exitCode !== 0) {
|
|
63
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
64
|
+
}
|
|
65
|
+
const info = JSON.parse(result.stdout);
|
|
66
|
+
return {
|
|
67
|
+
content: [{
|
|
68
|
+
type: 'text',
|
|
69
|
+
text: `Windows Update:\n Restart pending: ${info.RestartPending}\n Last check: ${info.LastCheckTime || 'Unknown'}`,
|
|
70
|
+
}],
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
server.tool(
|
|
76
|
+
'windows_event_log',
|
|
77
|
+
'Read Windows Event Log entries. Filter by log, level, source, or time.',
|
|
78
|
+
{
|
|
79
|
+
log: z.string().default('System').describe('Log name (System, Application, Security, etc.)'),
|
|
80
|
+
level: z.enum(['Critical', 'Error', 'Warning', 'Information', 'All']).default('All').describe('Event level'),
|
|
81
|
+
source: z.string().optional().describe('Event source filter'),
|
|
82
|
+
limit: z.number().default(20).describe('Max entries'),
|
|
83
|
+
hours: z.number().optional().describe('Only events from last N hours'),
|
|
84
|
+
},
|
|
85
|
+
async ({ log, level, source, limit, hours }) => {
|
|
86
|
+
const levelMap: Record<string, string> = {
|
|
87
|
+
Critical: '1', Error: '2', Warning: '3', Information: '4',
|
|
88
|
+
};
|
|
89
|
+
const levelFilter = level !== 'All' ? `-Level ${levelMap[level]}` : '';
|
|
90
|
+
const sourceFilter = source ? `-ProviderName '${source.replace(/'/g, "''")}'` : '';
|
|
91
|
+
const timeFilter = hours ? `-After (Get-Date).AddHours(-${hours})` : '';
|
|
92
|
+
|
|
93
|
+
const ps = `
|
|
94
|
+
Get-WinEvent -LogName '${log.replace(/'/g, "''")}' -MaxEvents ${limit} ${levelFilter ? `| Where-Object { $_.Level -eq ${levelMap[level]} }` : ''} -ErrorAction SilentlyContinue |
|
|
95
|
+
${source ? `Where-Object { $_.ProviderName -like '*${source.replace(/'/g, "''")}*' } |` : ''}
|
|
96
|
+
${hours ? `Where-Object { $_.TimeCreated -gt (Get-Date).AddHours(-${hours}) } |` : ''}
|
|
97
|
+
Select-Object -First ${limit} |
|
|
98
|
+
ForEach-Object {
|
|
99
|
+
[PSCustomObject]@{
|
|
100
|
+
Time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
|
|
101
|
+
Level = $_.LevelDisplayName
|
|
102
|
+
Source = $_.ProviderName
|
|
103
|
+
EventId = $_.Id
|
|
104
|
+
Message = ($_.Message -split [char]10)[0].Substring(0, [math]::Min(($_.Message -split [char]10)[0].Length, 100))
|
|
105
|
+
}
|
|
106
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
107
|
+
|
|
108
|
+
const result = await runPowerShell(ps, { timeout: 20000 });
|
|
109
|
+
if (result.exitCode !== 0) {
|
|
110
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
111
|
+
}
|
|
112
|
+
if (!result.stdout) {
|
|
113
|
+
return { content: [{ type: 'text', text: 'No events found.' }] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const events = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
117
|
+
const lines = events.map((e: { Time: string; Level: string; Source: string; EventId: number; Message: string }) =>
|
|
118
|
+
`${(e.Level || '').padEnd(12)} ${e.Time} ${String(e.EventId).padStart(5)} ${(e.Source || '').padEnd(25).slice(0, 25)} ${(e.Message || '').slice(0, 50)}`,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const header = `${'Level'.padEnd(12)} ${'Time'.padEnd(19)} ${'ID'.padStart(5)} ${'Source'.padEnd(25)} Message`;
|
|
122
|
+
return { content: [{ type: 'text', text: `${log} log:\n${header}\n${'─'.repeat(120)}\n${lines.join('\n')}` }] };
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
'windows_restore_point',
|
|
128
|
+
'List or create System Restore points.',
|
|
129
|
+
{
|
|
130
|
+
action: z.enum(['list', 'create']).default('list').describe('Action'),
|
|
131
|
+
description: z.string().optional().describe('Description for new restore point'),
|
|
132
|
+
},
|
|
133
|
+
async ({ action, description }) => {
|
|
134
|
+
if (action === 'create') {
|
|
135
|
+
const desc = description || 'mcp_windows restore point';
|
|
136
|
+
const ps = `Checkpoint-Computer -Description '${desc.replace(/'/g, "''")}' -RestorePointType MODIFY_SETTINGS -ErrorAction Stop; "Restore point created: ${desc}"`;
|
|
137
|
+
const result = await runPowerShell(ps, { timeout: 60000 });
|
|
138
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const ps = `
|
|
142
|
+
try {
|
|
143
|
+
$points = Get-ComputerRestorePoint -ErrorAction Stop
|
|
144
|
+
if (-not $points) { Write-Output '[]'; return }
|
|
145
|
+
$points | ForEach-Object {
|
|
146
|
+
[PSCustomObject]@{
|
|
147
|
+
SequenceNumber = $_.SequenceNumber
|
|
148
|
+
Description = $_.Description
|
|
149
|
+
Type = switch ($_.RestorePointType) { 0 {'Application Install'} 1 {'Application Uninstall'} 10 {'Device Install'} 12 {'Modify Settings'} 13 {'Cancel'} default {'Other'} }
|
|
150
|
+
Date = $_.ConvertToDateTime($_.CreationTime).ToString('yyyy-MM-dd HH:mm')
|
|
151
|
+
}
|
|
152
|
+
} | ConvertTo-Json -Depth 3 -Compress
|
|
153
|
+
} catch {
|
|
154
|
+
Write-Output "RESTORE_ERROR:$($_.Exception.Message)"
|
|
155
|
+
}`;
|
|
156
|
+
|
|
157
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
158
|
+
if (result.stdout?.startsWith('RESTORE_ERROR:')) {
|
|
159
|
+
const msg = result.stdout.replace('RESTORE_ERROR:', '');
|
|
160
|
+
return { content: [{ type: 'text', text: `System Restore: ${msg || 'Requires elevation or System Restore is disabled.'}` }] };
|
|
161
|
+
}
|
|
162
|
+
if (result.exitCode !== 0) {
|
|
163
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr || 'Requires elevation.'}` }], isError: true };
|
|
164
|
+
}
|
|
165
|
+
if (!result.stdout || result.stdout === '[]') {
|
|
166
|
+
return { content: [{ type: 'text', text: 'No restore points found (System Restore may be disabled or requires elevation).' }] };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const points = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
170
|
+
const lines = points.map((p: { SequenceNumber: number; Description: string; Type: string; Date: string }) =>
|
|
171
|
+
` #${p.SequenceNumber} ${p.Date} ${(p.Type || '').padEnd(20)} ${p.Description}`,
|
|
172
|
+
);
|
|
173
|
+
return { content: [{ type: 'text', text: `System Restore points:\n${lines.join('\n')}` }] };
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
server.tool(
|
|
178
|
+
'windows_certificate_list',
|
|
179
|
+
'List certificates in the Windows certificate store.',
|
|
180
|
+
{
|
|
181
|
+
store: z.string().default('Cert:\\CurrentUser\\My').describe('Certificate store path'),
|
|
182
|
+
expiring_days: z.number().optional().describe('Only show certs expiring within N days'),
|
|
183
|
+
filter: z.string().optional().describe('Filter by subject (substring)'),
|
|
184
|
+
},
|
|
185
|
+
async ({ store, expiring_days, filter }) => {
|
|
186
|
+
const expiryFilter = expiring_days
|
|
187
|
+
? `| Where-Object { $_.NotAfter -lt (Get-Date).AddDays(${expiring_days}) -and $_.NotAfter -gt (Get-Date) }`
|
|
188
|
+
: '';
|
|
189
|
+
const nameFilter = filter
|
|
190
|
+
? `| Where-Object { $_.Subject -like '*${filter.replace(/'/g, "''")}*' }`
|
|
191
|
+
: '';
|
|
192
|
+
|
|
193
|
+
// Map store path to .NET enums
|
|
194
|
+
// Cert:\CurrentUser\My -> CurrentUser, My
|
|
195
|
+
const storeMatch = store.match(/Cert:\\\\?(CurrentUser|LocalMachine)\\\\?(\w+)/i);
|
|
196
|
+
const storeLocation = storeMatch ? storeMatch[1] : 'CurrentUser';
|
|
197
|
+
const storeName = storeMatch ? storeMatch[2] : 'My';
|
|
198
|
+
|
|
199
|
+
const ps = `
|
|
200
|
+
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('${storeName}', '${storeLocation}')
|
|
201
|
+
$store.Open('ReadOnly')
|
|
202
|
+
$certs = $store.Certificates
|
|
203
|
+
${expiring_days ? `$certs = $certs | Where-Object { $_.NotAfter -lt (Get-Date).AddDays(${expiring_days}) -and $_.NotAfter -gt (Get-Date) }` : ''}
|
|
204
|
+
${filter ? `$certs = $certs | Where-Object { $_.Subject -like '*${filter.replace(/'/g, "''")}*' }` : ''}
|
|
205
|
+
$certs | ForEach-Object {
|
|
206
|
+
[PSCustomObject]@{
|
|
207
|
+
Subject = $_.Subject
|
|
208
|
+
Issuer = $_.Issuer
|
|
209
|
+
Thumbprint = $_.Thumbprint
|
|
210
|
+
Expires = $_.NotAfter.ToString('yyyy-MM-dd')
|
|
211
|
+
KeyUsage = ($_.Extensions | Where-Object { $_ -is [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension] } | ForEach-Object { ($_.EnhancedKeyUsages | ForEach-Object { $_.FriendlyName }) -join ', ' })
|
|
212
|
+
}
|
|
213
|
+
} | ConvertTo-Json -Depth 3 -Compress
|
|
214
|
+
$store.Close()`;
|
|
215
|
+
|
|
216
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
217
|
+
if (result.exitCode !== 0) {
|
|
218
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
219
|
+
}
|
|
220
|
+
if (!result.stdout) {
|
|
221
|
+
return { content: [{ type: 'text', text: 'No certificates found.' }] };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const certs = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
225
|
+
const lines = certs.map((c: { Subject: string; Expires: string; Thumbprint: string; KeyUsage: string }) =>
|
|
226
|
+
` ${c.Expires} ${(c.Thumbprint || '').slice(0, 16)}... ${(c.Subject || '').slice(0, 60)}`,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const header = ` ${'Expires'.padEnd(10)} ${'Thumbprint'.padEnd(19)} Subject`;
|
|
230
|
+
return { content: [{ type: 'text', text: `${store}\n${header}\n${'─'.repeat(90)}\n${lines.join('\n')}\n\n${certs.length} certificates` }] };
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
server.tool(
|
|
235
|
+
'windows_performance_monitor',
|
|
236
|
+
'Get real-time system performance: CPU per core, RAM, disk I/O, network throughput, top processes.',
|
|
237
|
+
{},
|
|
238
|
+
async () => {
|
|
239
|
+
const ps = `
|
|
240
|
+
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
|
241
|
+
$os = Get-CimInstance Win32_OperatingSystem
|
|
242
|
+
$cs = Get-CimInstance Win32_ComputerSystem
|
|
243
|
+
$perfDisk = Get-CimInstance Win32_PerfFormattedData_PerfDisk_LogicalDisk -Filter "Name='_Total'" -ErrorAction SilentlyContinue
|
|
244
|
+
$perfNet = Get-CimInstance Win32_PerfFormattedData_Tcpip_NetworkInterface -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
245
|
+
|
|
246
|
+
$topCPU = Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 | ForEach-Object {
|
|
247
|
+
[PSCustomObject]@{ Name = $_.ProcessName; PID = $_.Id; CPU = [math]::Round($_.CPU, 1); MemMB = [math]::Round($_.WorkingSet64 / 1MB, 1) }
|
|
248
|
+
}
|
|
249
|
+
$topMem = Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 5 | ForEach-Object {
|
|
250
|
+
[PSCustomObject]@{ Name = $_.ProcessName; PID = $_.Id; MemMB = [math]::Round($_.WorkingSet64 / 1MB, 1) }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
[PSCustomObject]@{
|
|
254
|
+
CPUUsage = "$([math]::Round($cpu.LoadPercentage, 0))%"
|
|
255
|
+
CPUCores = $cpu.NumberOfLogicalProcessors
|
|
256
|
+
RAMTotalGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
|
|
257
|
+
RAMUsedGB = [math]::Round(($cs.TotalPhysicalMemory - $os.FreePhysicalMemory * 1KB) / 1GB, 1)
|
|
258
|
+
RAMUsedPct = [math]::Round(($cs.TotalPhysicalMemory - $os.FreePhysicalMemory * 1KB) / $cs.TotalPhysicalMemory * 100, 1)
|
|
259
|
+
DiskReadBps = if ($perfDisk) { "$([math]::Round($perfDisk.DiskReadBytesPersec / 1MB, 2)) MB/s" } else { 'N/A' }
|
|
260
|
+
DiskWriteBps = if ($perfDisk) { "$([math]::Round($perfDisk.DiskWriteBytesPersec / 1MB, 2)) MB/s" } else { 'N/A' }
|
|
261
|
+
NetSentBps = if ($perfNet) { "$([math]::Round($perfNet.BytesSentPersec / 1MB, 2)) MB/s" } else { 'N/A' }
|
|
262
|
+
NetRecvBps = if ($perfNet) { "$([math]::Round($perfNet.BytesReceivedPersec / 1MB, 2)) MB/s" } else { 'N/A' }
|
|
263
|
+
TopCPU = $topCPU
|
|
264
|
+
TopMem = $topMem
|
|
265
|
+
} | ConvertTo-Json -Depth 4 -Compress`;
|
|
266
|
+
|
|
267
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
268
|
+
if (result.exitCode !== 0) {
|
|
269
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const p = JSON.parse(result.stdout);
|
|
273
|
+
const topCpu = Array.isArray(p.TopCPU) ? p.TopCPU : [p.TopCPU].filter(Boolean);
|
|
274
|
+
const topMem = Array.isArray(p.TopMem) ? p.TopMem : [p.TopMem].filter(Boolean);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
content: [{
|
|
278
|
+
type: 'text',
|
|
279
|
+
text: [
|
|
280
|
+
`CPU: ${p.CPUUsage} (${p.CPUCores} threads)`,
|
|
281
|
+
`RAM: ${p.RAMUsedGB}/${p.RAMTotalGB} GB (${p.RAMUsedPct}%)`,
|
|
282
|
+
`Disk: Read ${p.DiskReadBps} / Write ${p.DiskWriteBps}`,
|
|
283
|
+
`Net: Send ${p.NetSentBps} / Recv ${p.NetRecvBps}`,
|
|
284
|
+
'',
|
|
285
|
+
'Top by CPU:',
|
|
286
|
+
...topCpu.map((t: { Name: string; PID: number; CPU: number; MemMB: number }) =>
|
|
287
|
+
` ${String(t.PID).padStart(6)} ${t.Name.padEnd(20)} ${String(t.CPU).padStart(8)}s ${String(t.MemMB).padStart(8)} MB`,
|
|
288
|
+
),
|
|
289
|
+
'',
|
|
290
|
+
'Top by Memory:',
|
|
291
|
+
...topMem.map((t: { Name: string; PID: number; MemMB: number }) =>
|
|
292
|
+
` ${String(t.PID).padStart(6)} ${t.Name.padEnd(20)} ${String(t.MemMB).padStart(8)} MB`,
|
|
293
|
+
),
|
|
294
|
+
].join('\n'),
|
|
295
|
+
}],
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_network_connections (#23)
|
|
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 registerNetstatTools(server: McpServer): void {
|
|
12
|
+
server.tool(
|
|
13
|
+
'windows_network_connections',
|
|
14
|
+
'List active TCP/UDP network connections (like netstat). Shows local/remote address, state, and owning process.',
|
|
15
|
+
{
|
|
16
|
+
state: z.enum(['all', 'listen', 'established', 'time_wait', 'close_wait']).default('all').describe('Filter by state'),
|
|
17
|
+
port: z.number().optional().describe('Filter by port number'),
|
|
18
|
+
process_name: z.string().optional().describe('Filter by process name'),
|
|
19
|
+
limit: z.number().default(50).describe('Max results'),
|
|
20
|
+
},
|
|
21
|
+
async ({ state, port, process_name, limit }) => {
|
|
22
|
+
const stateMap: Record<string, string> = {
|
|
23
|
+
listen: "| Where-Object { $_.State -eq 'Listen' }",
|
|
24
|
+
established: "| Where-Object { $_.State -eq 'Established' }",
|
|
25
|
+
time_wait: "| Where-Object { $_.State -eq 'TimeWait' }",
|
|
26
|
+
close_wait: "| Where-Object { $_.State -eq 'CloseWait' }",
|
|
27
|
+
all: '',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const portFilter = port
|
|
31
|
+
? `| Where-Object { $_.LocalPort -eq ${port} -or $_.RemotePort -eq ${port} }`
|
|
32
|
+
: '';
|
|
33
|
+
|
|
34
|
+
const procFilter = process_name
|
|
35
|
+
? `| Where-Object { $procName -like '*${process_name.replace(/'/g, "''")}*' }`
|
|
36
|
+
: '';
|
|
37
|
+
|
|
38
|
+
const ps = `
|
|
39
|
+
Get-NetTCPConnection -ErrorAction SilentlyContinue ${stateMap[state]} ${portFilter} | ForEach-Object {
|
|
40
|
+
$proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
|
|
41
|
+
$procName = if ($proc) { $proc.ProcessName } else { '?' }
|
|
42
|
+
[PSCustomObject]@{
|
|
43
|
+
Proto = 'TCP'
|
|
44
|
+
LocalAddr = "$($_.LocalAddress):$($_.LocalPort)"
|
|
45
|
+
RemoteAddr = "$($_.RemoteAddress):$($_.RemotePort)"
|
|
46
|
+
State = $_.State.ToString()
|
|
47
|
+
PID = $_.OwningProcess
|
|
48
|
+
Process = $procName
|
|
49
|
+
}
|
|
50
|
+
} ${procFilter} | Select-Object -First ${limit} | ConvertTo-Json -Depth 3 -Compress`;
|
|
51
|
+
|
|
52
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
53
|
+
if (result.exitCode !== 0) {
|
|
54
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!result.stdout) {
|
|
58
|
+
return { content: [{ type: 'text', text: 'No connections found.' }] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const conns = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
62
|
+
const lines = conns.map((c: { Proto: string; LocalAddr: string; RemoteAddr: string; State: string; PID: number; Process: string }) =>
|
|
63
|
+
`${c.Proto} ${c.LocalAddr.padEnd(22)} ${c.RemoteAddr.padEnd(22)} ${c.State.padEnd(12)} ${String(c.PID).padStart(6)} ${c.Process}`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const header = `Proto ${'Local Address'.padEnd(22)} ${'Remote Address'.padEnd(22)} ${'State'.padEnd(12)} ${'PID'.padStart(6)} Process`;
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: `${header}\n${'─'.repeat(100)}\n${lines.join('\n')}\n\n${conns.length} connections` }],
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|