@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,236 @@
|
|
|
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
|
+
import { z } from 'zod';
|
|
9
|
+
import { runPowerShell } from '../shell.js';
|
|
10
|
+
export function registerMaintenanceTools(server) {
|
|
11
|
+
server.tool('windows_updates', 'Check Windows Update status, pending updates, and recent history.', {
|
|
12
|
+
action: z.enum(['status', 'history']).default('status').describe('Check status or view history'),
|
|
13
|
+
limit: z.number().default(20).describe('Max history entries'),
|
|
14
|
+
}, async ({ action, limit }) => {
|
|
15
|
+
if (action === 'history') {
|
|
16
|
+
const ps = `
|
|
17
|
+
$session = New-Object -ComObject Microsoft.Update.Session
|
|
18
|
+
$searcher = $session.CreateUpdateSearcher()
|
|
19
|
+
$count = $searcher.GetTotalHistoryCount()
|
|
20
|
+
$history = $searcher.QueryHistory(0, [math]::Min($count, ${limit}))
|
|
21
|
+
$history | ForEach-Object {
|
|
22
|
+
[PSCustomObject]@{
|
|
23
|
+
Date = $_.Date.ToString('yyyy-MM-dd HH:mm')
|
|
24
|
+
Title = $_.Title
|
|
25
|
+
Result = switch ($_.ResultCode) { 0 {'Not Started'} 1 {'In Progress'} 2 {'Succeeded'} 3 {'Succeeded with Errors'} 4 {'Failed'} 5 {'Aborted'} default {'Unknown'} }
|
|
26
|
+
}
|
|
27
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
28
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
29
|
+
if (result.exitCode !== 0) {
|
|
30
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
31
|
+
}
|
|
32
|
+
if (!result.stdout) {
|
|
33
|
+
return { content: [{ type: 'text', text: 'No update history.' }] };
|
|
34
|
+
}
|
|
35
|
+
const entries = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
36
|
+
const lines = entries.map((e) => `${e.Result.padEnd(10)} ${e.Date} ${(e.Title || '').slice(0, 70)}`);
|
|
37
|
+
return { content: [{ type: 'text', text: `Recent updates:\n${lines.join('\n')}` }] };
|
|
38
|
+
}
|
|
39
|
+
// Status
|
|
40
|
+
const ps = `
|
|
41
|
+
$reboot = Test-Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired'
|
|
42
|
+
$lastCheck = (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\Results\\Detect' -Name LastSuccessTime -ErrorAction SilentlyContinue).LastSuccessTime
|
|
43
|
+
[PSCustomObject]@{
|
|
44
|
+
RestartPending = $reboot
|
|
45
|
+
LastCheckTime = $lastCheck
|
|
46
|
+
} | ConvertTo-Json -Compress`;
|
|
47
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
48
|
+
if (result.exitCode !== 0) {
|
|
49
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
50
|
+
}
|
|
51
|
+
const info = JSON.parse(result.stdout);
|
|
52
|
+
return {
|
|
53
|
+
content: [{
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: `Windows Update:\n Restart pending: ${info.RestartPending}\n Last check: ${info.LastCheckTime || 'Unknown'}`,
|
|
56
|
+
}],
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
server.tool('windows_event_log', 'Read Windows Event Log entries. Filter by log, level, source, or time.', {
|
|
60
|
+
log: z.string().default('System').describe('Log name (System, Application, Security, etc.)'),
|
|
61
|
+
level: z.enum(['Critical', 'Error', 'Warning', 'Information', 'All']).default('All').describe('Event level'),
|
|
62
|
+
source: z.string().optional().describe('Event source filter'),
|
|
63
|
+
limit: z.number().default(20).describe('Max entries'),
|
|
64
|
+
hours: z.number().optional().describe('Only events from last N hours'),
|
|
65
|
+
}, async ({ log, level, source, limit, hours }) => {
|
|
66
|
+
const levelMap = {
|
|
67
|
+
Critical: '1', Error: '2', Warning: '3', Information: '4',
|
|
68
|
+
};
|
|
69
|
+
const levelFilter = level !== 'All' ? `-Level ${levelMap[level]}` : '';
|
|
70
|
+
const sourceFilter = source ? `-ProviderName '${source.replace(/'/g, "''")}'` : '';
|
|
71
|
+
const timeFilter = hours ? `-After (Get-Date).AddHours(-${hours})` : '';
|
|
72
|
+
const ps = `
|
|
73
|
+
Get-WinEvent -LogName '${log.replace(/'/g, "''")}' -MaxEvents ${limit} ${levelFilter ? `| Where-Object { $_.Level -eq ${levelMap[level]} }` : ''} -ErrorAction SilentlyContinue |
|
|
74
|
+
${source ? `Where-Object { $_.ProviderName -like '*${source.replace(/'/g, "''")}*' } |` : ''}
|
|
75
|
+
${hours ? `Where-Object { $_.TimeCreated -gt (Get-Date).AddHours(-${hours}) } |` : ''}
|
|
76
|
+
Select-Object -First ${limit} |
|
|
77
|
+
ForEach-Object {
|
|
78
|
+
[PSCustomObject]@{
|
|
79
|
+
Time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
|
|
80
|
+
Level = $_.LevelDisplayName
|
|
81
|
+
Source = $_.ProviderName
|
|
82
|
+
EventId = $_.Id
|
|
83
|
+
Message = ($_.Message -split [char]10)[0].Substring(0, [math]::Min(($_.Message -split [char]10)[0].Length, 100))
|
|
84
|
+
}
|
|
85
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
86
|
+
const result = await runPowerShell(ps, { timeout: 20000 });
|
|
87
|
+
if (result.exitCode !== 0) {
|
|
88
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
89
|
+
}
|
|
90
|
+
if (!result.stdout) {
|
|
91
|
+
return { content: [{ type: 'text', text: 'No events found.' }] };
|
|
92
|
+
}
|
|
93
|
+
const events = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
94
|
+
const lines = events.map((e) => `${(e.Level || '').padEnd(12)} ${e.Time} ${String(e.EventId).padStart(5)} ${(e.Source || '').padEnd(25).slice(0, 25)} ${(e.Message || '').slice(0, 50)}`);
|
|
95
|
+
const header = `${'Level'.padEnd(12)} ${'Time'.padEnd(19)} ${'ID'.padStart(5)} ${'Source'.padEnd(25)} Message`;
|
|
96
|
+
return { content: [{ type: 'text', text: `${log} log:\n${header}\n${'─'.repeat(120)}\n${lines.join('\n')}` }] };
|
|
97
|
+
});
|
|
98
|
+
server.tool('windows_restore_point', 'List or create System Restore points.', {
|
|
99
|
+
action: z.enum(['list', 'create']).default('list').describe('Action'),
|
|
100
|
+
description: z.string().optional().describe('Description for new restore point'),
|
|
101
|
+
}, async ({ action, description }) => {
|
|
102
|
+
if (action === 'create') {
|
|
103
|
+
const desc = description || 'mcp_windows restore point';
|
|
104
|
+
const ps = `Checkpoint-Computer -Description '${desc.replace(/'/g, "''")}' -RestorePointType MODIFY_SETTINGS -ErrorAction Stop; "Restore point created: ${desc}"`;
|
|
105
|
+
const result = await runPowerShell(ps, { timeout: 60000 });
|
|
106
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
|
|
107
|
+
}
|
|
108
|
+
const ps = `
|
|
109
|
+
try {
|
|
110
|
+
$points = Get-ComputerRestorePoint -ErrorAction Stop
|
|
111
|
+
if (-not $points) { Write-Output '[]'; return }
|
|
112
|
+
$points | ForEach-Object {
|
|
113
|
+
[PSCustomObject]@{
|
|
114
|
+
SequenceNumber = $_.SequenceNumber
|
|
115
|
+
Description = $_.Description
|
|
116
|
+
Type = switch ($_.RestorePointType) { 0 {'Application Install'} 1 {'Application Uninstall'} 10 {'Device Install'} 12 {'Modify Settings'} 13 {'Cancel'} default {'Other'} }
|
|
117
|
+
Date = $_.ConvertToDateTime($_.CreationTime).ToString('yyyy-MM-dd HH:mm')
|
|
118
|
+
}
|
|
119
|
+
} | ConvertTo-Json -Depth 3 -Compress
|
|
120
|
+
} catch {
|
|
121
|
+
Write-Output "RESTORE_ERROR:$($_.Exception.Message)"
|
|
122
|
+
}`;
|
|
123
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
124
|
+
if (result.stdout?.startsWith('RESTORE_ERROR:')) {
|
|
125
|
+
const msg = result.stdout.replace('RESTORE_ERROR:', '');
|
|
126
|
+
return { content: [{ type: 'text', text: `System Restore: ${msg || 'Requires elevation or System Restore is disabled.'}` }] };
|
|
127
|
+
}
|
|
128
|
+
if (result.exitCode !== 0) {
|
|
129
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr || 'Requires elevation.'}` }], isError: true };
|
|
130
|
+
}
|
|
131
|
+
if (!result.stdout || result.stdout === '[]') {
|
|
132
|
+
return { content: [{ type: 'text', text: 'No restore points found (System Restore may be disabled or requires elevation).' }] };
|
|
133
|
+
}
|
|
134
|
+
const points = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
135
|
+
const lines = points.map((p) => ` #${p.SequenceNumber} ${p.Date} ${(p.Type || '').padEnd(20)} ${p.Description}`);
|
|
136
|
+
return { content: [{ type: 'text', text: `System Restore points:\n${lines.join('\n')}` }] };
|
|
137
|
+
});
|
|
138
|
+
server.tool('windows_certificate_list', 'List certificates in the Windows certificate store.', {
|
|
139
|
+
store: z.string().default('Cert:\\CurrentUser\\My').describe('Certificate store path'),
|
|
140
|
+
expiring_days: z.number().optional().describe('Only show certs expiring within N days'),
|
|
141
|
+
filter: z.string().optional().describe('Filter by subject (substring)'),
|
|
142
|
+
}, async ({ store, expiring_days, filter }) => {
|
|
143
|
+
const expiryFilter = expiring_days
|
|
144
|
+
? `| Where-Object { $_.NotAfter -lt (Get-Date).AddDays(${expiring_days}) -and $_.NotAfter -gt (Get-Date) }`
|
|
145
|
+
: '';
|
|
146
|
+
const nameFilter = filter
|
|
147
|
+
? `| Where-Object { $_.Subject -like '*${filter.replace(/'/g, "''")}*' }`
|
|
148
|
+
: '';
|
|
149
|
+
// Map store path to .NET enums
|
|
150
|
+
// Cert:\CurrentUser\My -> CurrentUser, My
|
|
151
|
+
const storeMatch = store.match(/Cert:\\\\?(CurrentUser|LocalMachine)\\\\?(\w+)/i);
|
|
152
|
+
const storeLocation = storeMatch ? storeMatch[1] : 'CurrentUser';
|
|
153
|
+
const storeName = storeMatch ? storeMatch[2] : 'My';
|
|
154
|
+
const ps = `
|
|
155
|
+
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('${storeName}', '${storeLocation}')
|
|
156
|
+
$store.Open('ReadOnly')
|
|
157
|
+
$certs = $store.Certificates
|
|
158
|
+
${expiring_days ? `$certs = $certs | Where-Object { $_.NotAfter -lt (Get-Date).AddDays(${expiring_days}) -and $_.NotAfter -gt (Get-Date) }` : ''}
|
|
159
|
+
${filter ? `$certs = $certs | Where-Object { $_.Subject -like '*${filter.replace(/'/g, "''")}*' }` : ''}
|
|
160
|
+
$certs | ForEach-Object {
|
|
161
|
+
[PSCustomObject]@{
|
|
162
|
+
Subject = $_.Subject
|
|
163
|
+
Issuer = $_.Issuer
|
|
164
|
+
Thumbprint = $_.Thumbprint
|
|
165
|
+
Expires = $_.NotAfter.ToString('yyyy-MM-dd')
|
|
166
|
+
KeyUsage = ($_.Extensions | Where-Object { $_ -is [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension] } | ForEach-Object { ($_.EnhancedKeyUsages | ForEach-Object { $_.FriendlyName }) -join ', ' })
|
|
167
|
+
}
|
|
168
|
+
} | ConvertTo-Json -Depth 3 -Compress
|
|
169
|
+
$store.Close()`;
|
|
170
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
171
|
+
if (result.exitCode !== 0) {
|
|
172
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
173
|
+
}
|
|
174
|
+
if (!result.stdout) {
|
|
175
|
+
return { content: [{ type: 'text', text: 'No certificates found.' }] };
|
|
176
|
+
}
|
|
177
|
+
const certs = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
178
|
+
const lines = certs.map((c) => ` ${c.Expires} ${(c.Thumbprint || '').slice(0, 16)}... ${(c.Subject || '').slice(0, 60)}`);
|
|
179
|
+
const header = ` ${'Expires'.padEnd(10)} ${'Thumbprint'.padEnd(19)} Subject`;
|
|
180
|
+
return { content: [{ type: 'text', text: `${store}\n${header}\n${'─'.repeat(90)}\n${lines.join('\n')}\n\n${certs.length} certificates` }] };
|
|
181
|
+
});
|
|
182
|
+
server.tool('windows_performance_monitor', 'Get real-time system performance: CPU per core, RAM, disk I/O, network throughput, top processes.', {}, async () => {
|
|
183
|
+
const ps = `
|
|
184
|
+
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
|
185
|
+
$os = Get-CimInstance Win32_OperatingSystem
|
|
186
|
+
$cs = Get-CimInstance Win32_ComputerSystem
|
|
187
|
+
$perfDisk = Get-CimInstance Win32_PerfFormattedData_PerfDisk_LogicalDisk -Filter "Name='_Total'" -ErrorAction SilentlyContinue
|
|
188
|
+
$perfNet = Get-CimInstance Win32_PerfFormattedData_Tcpip_NetworkInterface -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
189
|
+
|
|
190
|
+
$topCPU = Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 | ForEach-Object {
|
|
191
|
+
[PSCustomObject]@{ Name = $_.ProcessName; PID = $_.Id; CPU = [math]::Round($_.CPU, 1); MemMB = [math]::Round($_.WorkingSet64 / 1MB, 1) }
|
|
192
|
+
}
|
|
193
|
+
$topMem = Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 5 | ForEach-Object {
|
|
194
|
+
[PSCustomObject]@{ Name = $_.ProcessName; PID = $_.Id; MemMB = [math]::Round($_.WorkingSet64 / 1MB, 1) }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
[PSCustomObject]@{
|
|
198
|
+
CPUUsage = "$([math]::Round($cpu.LoadPercentage, 0))%"
|
|
199
|
+
CPUCores = $cpu.NumberOfLogicalProcessors
|
|
200
|
+
RAMTotalGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
|
|
201
|
+
RAMUsedGB = [math]::Round(($cs.TotalPhysicalMemory - $os.FreePhysicalMemory * 1KB) / 1GB, 1)
|
|
202
|
+
RAMUsedPct = [math]::Round(($cs.TotalPhysicalMemory - $os.FreePhysicalMemory * 1KB) / $cs.TotalPhysicalMemory * 100, 1)
|
|
203
|
+
DiskReadBps = if ($perfDisk) { "$([math]::Round($perfDisk.DiskReadBytesPersec / 1MB, 2)) MB/s" } else { 'N/A' }
|
|
204
|
+
DiskWriteBps = if ($perfDisk) { "$([math]::Round($perfDisk.DiskWriteBytesPersec / 1MB, 2)) MB/s" } else { 'N/A' }
|
|
205
|
+
NetSentBps = if ($perfNet) { "$([math]::Round($perfNet.BytesSentPersec / 1MB, 2)) MB/s" } else { 'N/A' }
|
|
206
|
+
NetRecvBps = if ($perfNet) { "$([math]::Round($perfNet.BytesReceivedPersec / 1MB, 2)) MB/s" } else { 'N/A' }
|
|
207
|
+
TopCPU = $topCPU
|
|
208
|
+
TopMem = $topMem
|
|
209
|
+
} | ConvertTo-Json -Depth 4 -Compress`;
|
|
210
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
211
|
+
if (result.exitCode !== 0) {
|
|
212
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
213
|
+
}
|
|
214
|
+
const p = JSON.parse(result.stdout);
|
|
215
|
+
const topCpu = Array.isArray(p.TopCPU) ? p.TopCPU : [p.TopCPU].filter(Boolean);
|
|
216
|
+
const topMem = Array.isArray(p.TopMem) ? p.TopMem : [p.TopMem].filter(Boolean);
|
|
217
|
+
return {
|
|
218
|
+
content: [{
|
|
219
|
+
type: 'text',
|
|
220
|
+
text: [
|
|
221
|
+
`CPU: ${p.CPUUsage} (${p.CPUCores} threads)`,
|
|
222
|
+
`RAM: ${p.RAMUsedGB}/${p.RAMTotalGB} GB (${p.RAMUsedPct}%)`,
|
|
223
|
+
`Disk: Read ${p.DiskReadBps} / Write ${p.DiskWriteBps}`,
|
|
224
|
+
`Net: Send ${p.NetSentBps} / Recv ${p.NetRecvBps}`,
|
|
225
|
+
'',
|
|
226
|
+
'Top by CPU:',
|
|
227
|
+
...topCpu.map((t) => ` ${String(t.PID).padStart(6)} ${t.Name.padEnd(20)} ${String(t.CPU).padStart(8)}s ${String(t.MemMB).padStart(8)} MB`),
|
|
228
|
+
'',
|
|
229
|
+
'Top by Memory:',
|
|
230
|
+
...topMem.map((t) => ` ${String(t.PID).padStart(6)} ${t.Name.padEnd(20)} ${String(t.MemMB).padStart(8)} MB`),
|
|
231
|
+
].join('\n'),
|
|
232
|
+
}],
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=maintenance.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerNetstatTools(server) {
|
|
9
|
+
server.tool('windows_network_connections', 'List active TCP/UDP network connections (like netstat). Shows local/remote address, state, and owning process.', {
|
|
10
|
+
state: z.enum(['all', 'listen', 'established', 'time_wait', 'close_wait']).default('all').describe('Filter by state'),
|
|
11
|
+
port: z.number().optional().describe('Filter by port number'),
|
|
12
|
+
process_name: z.string().optional().describe('Filter by process name'),
|
|
13
|
+
limit: z.number().default(50).describe('Max results'),
|
|
14
|
+
}, async ({ state, port, process_name, limit }) => {
|
|
15
|
+
const stateMap = {
|
|
16
|
+
listen: "| Where-Object { $_.State -eq 'Listen' }",
|
|
17
|
+
established: "| Where-Object { $_.State -eq 'Established' }",
|
|
18
|
+
time_wait: "| Where-Object { $_.State -eq 'TimeWait' }",
|
|
19
|
+
close_wait: "| Where-Object { $_.State -eq 'CloseWait' }",
|
|
20
|
+
all: '',
|
|
21
|
+
};
|
|
22
|
+
const portFilter = port
|
|
23
|
+
? `| Where-Object { $_.LocalPort -eq ${port} -or $_.RemotePort -eq ${port} }`
|
|
24
|
+
: '';
|
|
25
|
+
const procFilter = process_name
|
|
26
|
+
? `| Where-Object { $procName -like '*${process_name.replace(/'/g, "''")}*' }`
|
|
27
|
+
: '';
|
|
28
|
+
const ps = `
|
|
29
|
+
Get-NetTCPConnection -ErrorAction SilentlyContinue ${stateMap[state]} ${portFilter} | ForEach-Object {
|
|
30
|
+
$proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
|
|
31
|
+
$procName = if ($proc) { $proc.ProcessName } else { '?' }
|
|
32
|
+
[PSCustomObject]@{
|
|
33
|
+
Proto = 'TCP'
|
|
34
|
+
LocalAddr = "$($_.LocalAddress):$($_.LocalPort)"
|
|
35
|
+
RemoteAddr = "$($_.RemoteAddress):$($_.RemotePort)"
|
|
36
|
+
State = $_.State.ToString()
|
|
37
|
+
PID = $_.OwningProcess
|
|
38
|
+
Process = $procName
|
|
39
|
+
}
|
|
40
|
+
} ${procFilter} | Select-Object -First ${limit} | ConvertTo-Json -Depth 3 -Compress`;
|
|
41
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
42
|
+
if (result.exitCode !== 0) {
|
|
43
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
44
|
+
}
|
|
45
|
+
if (!result.stdout) {
|
|
46
|
+
return { content: [{ type: 'text', text: 'No connections found.' }] };
|
|
47
|
+
}
|
|
48
|
+
const conns = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
49
|
+
const lines = conns.map((c) => `${c.Proto} ${c.LocalAddr.padEnd(22)} ${c.RemoteAddr.padEnd(22)} ${c.State.padEnd(12)} ${String(c.PID).padStart(6)} ${c.Process}`);
|
|
50
|
+
const header = `Proto ${'Local Address'.padEnd(22)} ${'Remote Address'.padEnd(22)} ${'State'.padEnd(12)} ${'PID'.padStart(6)} Process`;
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: `${header}\n${'─'.repeat(100)}\n${lines.join('\n')}\n\n${conns.length} connections` }],
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=netstat.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_network_info (#22)
|
|
5
|
+
*/
|
|
6
|
+
import { runPowerShell } from '../shell.js';
|
|
7
|
+
export function registerNetworkTools(server) {
|
|
8
|
+
server.tool('windows_network_info', 'Get network configuration: adapters, IPs, DNS, gateway, Wi-Fi status, and connectivity.', {}, async () => {
|
|
9
|
+
const ps = `
|
|
10
|
+
$adapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | ForEach-Object {
|
|
11
|
+
$ipInfo = Get-NetIPAddress -InterfaceIndex $_.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue
|
|
12
|
+
$dns = (Get-DnsClientServerAddress -InterfaceIndex $_.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue).ServerAddresses
|
|
13
|
+
$gw = (Get-NetRoute -InterfaceIndex $_.ifIndex -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue).NextHop
|
|
14
|
+
[PSCustomObject]@{
|
|
15
|
+
Name = $_.Name
|
|
16
|
+
Description = $_.InterfaceDescription
|
|
17
|
+
Type = $_.MediaType
|
|
18
|
+
Status = $_.Status
|
|
19
|
+
Speed = $_.LinkSpeed
|
|
20
|
+
MAC = $_.MacAddress
|
|
21
|
+
IP = if ($ipInfo) { $ipInfo.IPAddress -join ', ' } else { 'N/A' }
|
|
22
|
+
Subnet = if ($ipInfo) { $ipInfo.PrefixLength -join ', ' } else { 'N/A' }
|
|
23
|
+
Gateway = if ($gw) { $gw -join ', ' } else { 'N/A' }
|
|
24
|
+
DNS = if ($dns) { $dns -join ', ' } else { 'N/A' }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Wi-Fi info
|
|
29
|
+
$wifi = $null
|
|
30
|
+
try {
|
|
31
|
+
$wifiProfile = netsh wlan show interfaces 2>$null
|
|
32
|
+
if ($wifiProfile) {
|
|
33
|
+
$ssid = ($wifiProfile | Select-String 'SSID\s+:' | Select-Object -First 1) -replace '.*:\s*',''
|
|
34
|
+
$signal = ($wifiProfile | Select-String 'Signal' | Select-Object -First 1) -replace '.*:\s*',''
|
|
35
|
+
$auth = ($wifiProfile | Select-String 'Authentication' | Select-Object -First 1) -replace '.*:\s*',''
|
|
36
|
+
$wifi = [PSCustomObject]@{ SSID = $ssid.Trim(); Signal = $signal.Trim(); Security = $auth.Trim() }
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
|
|
40
|
+
# Connectivity
|
|
41
|
+
$connected = Test-Connection -ComputerName 8.8.8.8 -Count 1 -Quiet -ErrorAction SilentlyContinue
|
|
42
|
+
|
|
43
|
+
[PSCustomObject]@{
|
|
44
|
+
Adapters = $adapters
|
|
45
|
+
WiFi = $wifi
|
|
46
|
+
InternetConnected = $connected
|
|
47
|
+
} | ConvertTo-Json -Depth 4 -Compress`;
|
|
48
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
49
|
+
if (result.exitCode !== 0) {
|
|
50
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
51
|
+
}
|
|
52
|
+
const info = JSON.parse(result.stdout);
|
|
53
|
+
const lines = [];
|
|
54
|
+
lines.push(`Internet: ${info.InternetConnected ? 'Connected' : 'Disconnected'}`);
|
|
55
|
+
if (info.WiFi) {
|
|
56
|
+
lines.push(`Wi-Fi: ${info.WiFi.SSID} (${info.WiFi.Signal}, ${info.WiFi.Security})`);
|
|
57
|
+
}
|
|
58
|
+
lines.push('');
|
|
59
|
+
lines.push('Adapters:');
|
|
60
|
+
const adapters = Array.isArray(info.Adapters) ? info.Adapters : [info.Adapters];
|
|
61
|
+
for (const a of adapters.filter(Boolean)) {
|
|
62
|
+
lines.push(` ${a.Name} (${a.Description})`);
|
|
63
|
+
lines.push(` IP: ${a.IP}/${a.Subnet} Gateway: ${a.Gateway}`);
|
|
64
|
+
lines.push(` DNS: ${a.DNS}`);
|
|
65
|
+
lines.push(` MAC: ${a.MAC} Speed: ${a.Speed}`);
|
|
66
|
+
}
|
|
67
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=network.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_notification_send (#20)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerNotificationTools(server) {
|
|
9
|
+
server.tool('windows_notification_send', 'Send a Windows toast notification with title, body, and optional icon.', {
|
|
10
|
+
title: z.string().describe('Notification title'),
|
|
11
|
+
body: z.string().describe('Notification body text'),
|
|
12
|
+
icon: z.string().optional().describe('Path to icon file (optional)'),
|
|
13
|
+
sound: z.boolean().default(true).describe('Play notification sound'),
|
|
14
|
+
}, async ({ title, body, icon, sound }) => {
|
|
15
|
+
const iconParam = icon
|
|
16
|
+
? `$notify.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon('${icon.replace(/'/g, "''")}');`
|
|
17
|
+
: `$notify.Icon = [System.Drawing.SystemIcons]::Information;`;
|
|
18
|
+
const ps = `
|
|
19
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
20
|
+
Add-Type -AssemblyName System.Drawing
|
|
21
|
+
|
|
22
|
+
$notify = New-Object System.Windows.Forms.NotifyIcon
|
|
23
|
+
${iconParam}
|
|
24
|
+
$notify.Visible = $true
|
|
25
|
+
$notify.BalloonTipTitle = '${title.replace(/'/g, "''")}'
|
|
26
|
+
$notify.BalloonTipText = '${body.replace(/'/g, "''")}'
|
|
27
|
+
$notify.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Info
|
|
28
|
+
$notify.ShowBalloonTip(5000)
|
|
29
|
+
|
|
30
|
+
# Keep alive briefly so notification displays
|
|
31
|
+
Start-Sleep -Milliseconds 100
|
|
32
|
+
$notify.Dispose()
|
|
33
|
+
"Notification sent: ${title.replace(/"/g, '\\"')}"`;
|
|
34
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: result.stdout || result.stderr }],
|
|
37
|
+
isError: result.exitCode !== 0,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=notification.js.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_power_get (#12), windows_power_action (#13)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerPowerTools(server) {
|
|
9
|
+
server.tool('windows_power_get', 'Get power state: battery level, AC/battery, power plan, screen/sleep timeouts.', {}, async () => {
|
|
10
|
+
const ps = `
|
|
11
|
+
$battery = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue
|
|
12
|
+
$plan = powercfg /getactivescheme 2>$null
|
|
13
|
+
$planName = if ($plan) { ($plan -replace '^.*\\((.*)\\).*$','$1').Trim() } else { 'Unknown' }
|
|
14
|
+
|
|
15
|
+
# Get timeout values
|
|
16
|
+
$acScreen = (powercfg /query SCHEME_CURRENT SUB_VIDEO VIDEOIDLE 2>$null | Select-String 'Current AC Power Setting Index' | ForEach-Object { ($_ -split '0x')[1] }) -as [int]
|
|
17
|
+
$dcScreen = (powercfg /query SCHEME_CURRENT SUB_VIDEO VIDEOIDLE 2>$null | Select-String 'Current DC Power Setting Index' | ForEach-Object { ($_ -split '0x')[1] }) -as [int]
|
|
18
|
+
$acSleep = (powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE 2>$null | Select-String 'Current AC Power Setting Index' | ForEach-Object { ($_ -split '0x')[1] }) -as [int]
|
|
19
|
+
$dcSleep = (powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE 2>$null | Select-String 'Current DC Power Setting Index' | ForEach-Object { ($_ -split '0x')[1] }) -as [int]
|
|
20
|
+
|
|
21
|
+
[PSCustomObject]@{
|
|
22
|
+
HasBattery = $null -ne $battery
|
|
23
|
+
BatteryPct = if ($battery) { $battery.EstimatedChargeRemaining } else { $null }
|
|
24
|
+
Charging = if ($battery) { $battery.BatteryStatus -eq 2 } else { $null }
|
|
25
|
+
ACPower = if ($battery) { $battery.BatteryStatus -eq 2 -or $battery.BatteryStatus -eq 6 } else { $true }
|
|
26
|
+
TimeRemaining = if ($battery -and $battery.EstimatedRunTime -and $battery.EstimatedRunTime -lt 71582788) { "$([math]::Floor($battery.EstimatedRunTime / 60))h $($battery.EstimatedRunTime % 60)m" } else { $null }
|
|
27
|
+
PowerPlan = $planName
|
|
28
|
+
ScreenTimeout_AC = if ($acScreen) { "$([math]::Floor($acScreen / 60))m" } else { 'Never' }
|
|
29
|
+
ScreenTimeout_DC = if ($dcScreen) { "$([math]::Floor($dcScreen / 60))m" } else { 'Never' }
|
|
30
|
+
SleepTimeout_AC = if ($acSleep) { "$([math]::Floor($acSleep / 60))m" } else { 'Never' }
|
|
31
|
+
SleepTimeout_DC = if ($dcSleep) { "$([math]::Floor($dcSleep / 60))m" } else { 'Never' }
|
|
32
|
+
} | ConvertTo-Json -Compress`;
|
|
33
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
34
|
+
if (result.exitCode !== 0) {
|
|
35
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
36
|
+
}
|
|
37
|
+
const info = JSON.parse(result.stdout);
|
|
38
|
+
const lines = [];
|
|
39
|
+
if (info.HasBattery) {
|
|
40
|
+
const icon = info.Charging ? '🔌' : '🔋';
|
|
41
|
+
lines.push(`${icon} Battery: ${info.BatteryPct}%${info.Charging ? ' (charging)' : ''}`);
|
|
42
|
+
if (info.TimeRemaining)
|
|
43
|
+
lines.push(`Estimated remaining: ${info.TimeRemaining}`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
lines.push(`AC Power (no battery)`);
|
|
47
|
+
}
|
|
48
|
+
lines.push(`Power Plan: ${info.PowerPlan}`);
|
|
49
|
+
lines.push(``);
|
|
50
|
+
lines.push(`Screen timeout: ${info.ScreenTimeout_AC} (AC) / ${info.ScreenTimeout_DC} (battery)`);
|
|
51
|
+
lines.push(`Sleep timeout: ${info.SleepTimeout_AC} (AC) / ${info.SleepTimeout_DC} (battery)`);
|
|
52
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
53
|
+
});
|
|
54
|
+
server.tool('windows_power_action', 'Execute power actions: sleep, hibernate, lock, shutdown, restart, or switch power plan.', {
|
|
55
|
+
action: z.enum(['sleep', 'hibernate', 'lock', 'shutdown', 'restart', 'logoff', 'plan']).describe('Power action'),
|
|
56
|
+
delay: z.number().optional().describe('Delay in seconds (for shutdown/restart)'),
|
|
57
|
+
cancel: z.boolean().optional().describe('Cancel a scheduled shutdown/restart'),
|
|
58
|
+
plan: z.enum(['balanced', 'performance', 'powersaver']).optional().describe('Power plan to switch to (when action=plan)'),
|
|
59
|
+
}, async ({ action, delay, cancel, plan }) => {
|
|
60
|
+
if (cancel) {
|
|
61
|
+
const result = await runPowerShell('shutdown /a 2>&1; "Scheduled shutdown cancelled"');
|
|
62
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
63
|
+
}
|
|
64
|
+
let ps;
|
|
65
|
+
const delayArg = delay ? `/t ${delay}` : '';
|
|
66
|
+
switch (action) {
|
|
67
|
+
case 'sleep':
|
|
68
|
+
ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState('Suspend', $false, $false); "Sleeping..."`;
|
|
69
|
+
break;
|
|
70
|
+
case 'hibernate':
|
|
71
|
+
ps = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState('Hibernate', $false, $false); "Hibernating..."`;
|
|
72
|
+
break;
|
|
73
|
+
case 'lock':
|
|
74
|
+
ps = `rundll32.exe user32.dll,LockWorkStation; "Workstation locked"`;
|
|
75
|
+
break;
|
|
76
|
+
case 'shutdown':
|
|
77
|
+
ps = `shutdown /s /f ${delayArg}; "Shutdown initiated${delay ? ` in ${delay}s` : ''}"`;
|
|
78
|
+
break;
|
|
79
|
+
case 'restart':
|
|
80
|
+
ps = `shutdown /r /f ${delayArg}; "Restart initiated${delay ? ` in ${delay}s` : ''}"`;
|
|
81
|
+
break;
|
|
82
|
+
case 'logoff':
|
|
83
|
+
ps = `shutdown /l; "Logging off..."`;
|
|
84
|
+
break;
|
|
85
|
+
case 'plan':
|
|
86
|
+
if (!plan) {
|
|
87
|
+
return { content: [{ type: 'text', text: 'Specify a plan: balanced, performance, or powersaver' }], isError: true };
|
|
88
|
+
}
|
|
89
|
+
const planGuids = {
|
|
90
|
+
balanced: '381b4222-f694-41f0-9685-ff5bb260df2e',
|
|
91
|
+
performance: '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c',
|
|
92
|
+
powersaver: 'a1841308-3541-4fab-bc81-f71556f20b4a',
|
|
93
|
+
};
|
|
94
|
+
ps = `powercfg /setactive ${planGuids[plan]}; $p = powercfg /getactivescheme; "Switched to: $($p -replace '^.*\\((.*)\\).*$','$1')"`;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: result.stdout || result.stderr }],
|
|
100
|
+
isError: result.exitCode !== 0,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=power.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_printer_list (#50)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerPrinterTools(server) {
|
|
9
|
+
server.tool('windows_printer_list', 'List printers, show print queues, set default, or clear a queue.', {
|
|
10
|
+
action: z.enum(['list', 'queue', 'set_default', 'clear_queue']).default('list').describe('Action'),
|
|
11
|
+
printer: z.string().optional().describe('Printer name (for queue/set_default/clear_queue)'),
|
|
12
|
+
}, async ({ action, printer }) => {
|
|
13
|
+
switch (action) {
|
|
14
|
+
case 'list': {
|
|
15
|
+
const ps = `
|
|
16
|
+
Get-Printer -ErrorAction SilentlyContinue | ForEach-Object {
|
|
17
|
+
$default = if ((Get-CimInstance Win32_Printer -Filter "Name='$($_.Name.Replace("'","''"))'" -ErrorAction SilentlyContinue).Default) { $true } else { $false }
|
|
18
|
+
[PSCustomObject]@{
|
|
19
|
+
Name = $_.Name
|
|
20
|
+
Status = $_.PrinterStatus
|
|
21
|
+
Type = $_.Type
|
|
22
|
+
Port = $_.PortName
|
|
23
|
+
Driver = $_.DriverName
|
|
24
|
+
Default = $default
|
|
25
|
+
Shared = $_.Shared
|
|
26
|
+
}
|
|
27
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
28
|
+
const result = await runPowerShell(ps, { timeout: 45000 });
|
|
29
|
+
if (result.exitCode !== 0) {
|
|
30
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
31
|
+
}
|
|
32
|
+
if (!result.stdout) {
|
|
33
|
+
return { content: [{ type: 'text', text: 'No printers found.' }] };
|
|
34
|
+
}
|
|
35
|
+
const printers = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
36
|
+
const lines = printers.map((p) => {
|
|
37
|
+
const def = p.Default ? ' *' : ' ';
|
|
38
|
+
return `${def} ${(p.Name || '').padEnd(35).slice(0, 35)} ${(p.Driver || '').padEnd(25).slice(0, 25)} ${p.Port || ''}`;
|
|
39
|
+
});
|
|
40
|
+
const header = ` ${'Name'.padEnd(35)} ${'Driver'.padEnd(25)} Port`;
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: `${header}\n${'─'.repeat(80)}\n${lines.join('\n')}\n\n${printers.length} printers (* = default)` }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
case 'queue': {
|
|
46
|
+
if (!printer) {
|
|
47
|
+
return { content: [{ type: 'text', text: 'Queue requires printer name.' }], isError: true };
|
|
48
|
+
}
|
|
49
|
+
const ps = `
|
|
50
|
+
Get-PrintJob -PrinterName '${printer.replace(/'/g, "''")}' -ErrorAction Stop | ForEach-Object {
|
|
51
|
+
[PSCustomObject]@{
|
|
52
|
+
Id = $_.Id
|
|
53
|
+
Document = $_.DocumentName
|
|
54
|
+
Status = $_.JobStatus
|
|
55
|
+
Pages = $_.TotalPages
|
|
56
|
+
Size = "$([math]::Round($_.Size / 1KB, 1)) KB"
|
|
57
|
+
Submitted = $_.SubmittedTime.ToString('yyyy-MM-dd HH:mm')
|
|
58
|
+
}
|
|
59
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
60
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
61
|
+
if (result.exitCode !== 0) {
|
|
62
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
63
|
+
}
|
|
64
|
+
if (!result.stdout) {
|
|
65
|
+
return { content: [{ type: 'text', text: `Print queue for "${printer}" is empty.` }] };
|
|
66
|
+
}
|
|
67
|
+
const jobs = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
68
|
+
const lines = jobs.map((j) => ` #${j.Id} ${(j.Document || '').padEnd(30).slice(0, 30)} ${(j.Status || '').padEnd(12)} ${j.Pages} pg ${j.Size}`);
|
|
69
|
+
return { content: [{ type: 'text', text: `Queue for "${printer}":\n${lines.join('\n')}` }] };
|
|
70
|
+
}
|
|
71
|
+
case 'set_default': {
|
|
72
|
+
if (!printer) {
|
|
73
|
+
return { content: [{ type: 'text', text: 'set_default requires printer name.' }], isError: true };
|
|
74
|
+
}
|
|
75
|
+
const ps = `
|
|
76
|
+
$p = Get-CimInstance Win32_Printer -Filter "Name='${printer.replace(/'/g, "''")}'" -ErrorAction Stop
|
|
77
|
+
Invoke-CimMethod -InputObject $p -MethodName SetDefaultPrinter | Out-Null
|
|
78
|
+
"Default printer set to: ${printer}"`;
|
|
79
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
80
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
81
|
+
}
|
|
82
|
+
case 'clear_queue': {
|
|
83
|
+
if (!printer) {
|
|
84
|
+
return { content: [{ type: 'text', text: 'clear_queue requires printer name.' }], isError: true };
|
|
85
|
+
}
|
|
86
|
+
const ps = `
|
|
87
|
+
$jobs = Get-PrintJob -PrinterName '${printer.replace(/'/g, "''")}' -ErrorAction Stop
|
|
88
|
+
$count = @($jobs).Count
|
|
89
|
+
$jobs | Remove-PrintJob -ErrorAction SilentlyContinue
|
|
90
|
+
"Cleared $count job(s) from ${printer}"`;
|
|
91
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
92
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=printer.js.map
|