@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,256 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_display_get (#9), windows_display_set (#10), windows_screenshot (#11)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
export function registerDisplayTools(server) {
|
|
12
|
+
server.tool('windows_display_get', 'Get display configuration: resolution, refresh rate, scaling, multi-monitor layout, HDR status.', {}, async () => {
|
|
13
|
+
const ps = `
|
|
14
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
15
|
+
|
|
16
|
+
$screens = [System.Windows.Forms.Screen]::AllScreens
|
|
17
|
+
$monitors = Get-CimInstance -Namespace root\\wmi -ClassName WmiMonitorBasicDisplayParams -ErrorAction SilentlyContinue
|
|
18
|
+
$videoCtrl = Get-CimInstance Win32_VideoController
|
|
19
|
+
|
|
20
|
+
$i = 0
|
|
21
|
+
$screens | ForEach-Object {
|
|
22
|
+
$s = $_
|
|
23
|
+
$vc = $videoCtrl | Where-Object { $_.Name -match $s.DeviceName -or $true } | Select-Object -First 1
|
|
24
|
+
$dpiScale = [math]::Round(($s.Bounds.Width / $s.WorkingArea.Width) * 100, 0)
|
|
25
|
+
|
|
26
|
+
# Try to get real scaling from registry
|
|
27
|
+
$regScale = $null
|
|
28
|
+
try {
|
|
29
|
+
$regPath = "HKCU:\\Control Panel\\Desktop\\PerMonitorSettings"
|
|
30
|
+
if (Test-Path $regPath) {
|
|
31
|
+
$regScale = (Get-ChildItem $regPath -ErrorAction SilentlyContinue | Select-Object -Index $i | Get-ItemProperty -ErrorAction SilentlyContinue).DpiValue
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
|
|
35
|
+
[PSCustomObject]@{
|
|
36
|
+
Index = $i
|
|
37
|
+
Name = $s.DeviceName
|
|
38
|
+
Primary = $s.Primary
|
|
39
|
+
Resolution = "$($s.Bounds.Width)x$($s.Bounds.Height)"
|
|
40
|
+
RefreshRate = if ($vc) { "$($vc.CurrentRefreshRate) Hz" } else { 'Unknown' }
|
|
41
|
+
Scaling = if ($regScale) { "$($regScale)%" } else { 'System default' }
|
|
42
|
+
Position = "($($s.Bounds.X), $($s.Bounds.Y))"
|
|
43
|
+
WorkArea = "$($s.WorkingArea.Width)x$($s.WorkingArea.Height)"
|
|
44
|
+
BitsPerPixel = if ($vc) { $vc.CurrentBitsPerPixel } else { $null }
|
|
45
|
+
}
|
|
46
|
+
$i++
|
|
47
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
48
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
49
|
+
if (result.exitCode !== 0) {
|
|
50
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
51
|
+
}
|
|
52
|
+
const displays = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
53
|
+
const lines = displays.map((d) => [
|
|
54
|
+
`Monitor ${d.Index}: ${d.Name}${d.Primary ? ' (Primary)' : ''}`,
|
|
55
|
+
` Resolution: ${d.Resolution} @ ${d.RefreshRate}`,
|
|
56
|
+
` Scaling: ${d.Scaling}`,
|
|
57
|
+
` Position: ${d.Position}`,
|
|
58
|
+
` Work area: ${d.WorkArea}`,
|
|
59
|
+
].join('\n'));
|
|
60
|
+
return { content: [{ type: 'text', text: lines.join('\n\n') }] };
|
|
61
|
+
});
|
|
62
|
+
server.tool('windows_display_set', 'Change display settings: resolution, brightness.', {
|
|
63
|
+
resolution: z.string().optional().describe('Resolution as "WIDTHxHEIGHT" (e.g. "1920x1080")'),
|
|
64
|
+
brightness: z.number().min(0).max(100).optional().describe('Screen brightness 0-100 (laptops only)'),
|
|
65
|
+
}, async ({ resolution, brightness }) => {
|
|
66
|
+
const results = [];
|
|
67
|
+
if (brightness !== undefined) {
|
|
68
|
+
const ps = `
|
|
69
|
+
try {
|
|
70
|
+
$monitors = Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightnessMethods -ErrorAction Stop
|
|
71
|
+
$monitors | Invoke-CimMethod -MethodName WmiSetBrightness -Arguments @{Timeout=1; Brightness=${brightness}} -ErrorAction Stop
|
|
72
|
+
"Brightness set to ${brightness}%"
|
|
73
|
+
} catch {
|
|
74
|
+
"Error: Brightness control not available (requires laptop/integrated display). $_"
|
|
75
|
+
}`;
|
|
76
|
+
const r = await runPowerShell(ps);
|
|
77
|
+
results.push(r.stdout || r.stderr);
|
|
78
|
+
}
|
|
79
|
+
if (resolution) {
|
|
80
|
+
const match = resolution.match(/^(\d+)x(\d+)$/);
|
|
81
|
+
if (!match) {
|
|
82
|
+
results.push('Invalid resolution format. Use WIDTHxHEIGHT (e.g. 1920x1080)');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const ps = `
|
|
86
|
+
Add-Type @'
|
|
87
|
+
using System;
|
|
88
|
+
using System.Runtime.InteropServices;
|
|
89
|
+
|
|
90
|
+
public class DisplaySettings {
|
|
91
|
+
[DllImport("user32.dll")]
|
|
92
|
+
public static extern int EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
|
|
93
|
+
[DllImport("user32.dll")]
|
|
94
|
+
public static extern int ChangeDisplaySettings(ref DEVMODE devMode, int flags);
|
|
95
|
+
|
|
96
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
|
97
|
+
public struct DEVMODE {
|
|
98
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
|
99
|
+
public string dmDeviceName;
|
|
100
|
+
public short dmSpecVersion;
|
|
101
|
+
public short dmDriverVersion;
|
|
102
|
+
public short dmSize;
|
|
103
|
+
public short dmDriverExtra;
|
|
104
|
+
public int dmFields;
|
|
105
|
+
public int dmPositionX;
|
|
106
|
+
public int dmPositionY;
|
|
107
|
+
public int dmDisplayOrientation;
|
|
108
|
+
public int dmDisplayFixedOutput;
|
|
109
|
+
public short dmColor;
|
|
110
|
+
public short dmDuplex;
|
|
111
|
+
public short dmYResolution;
|
|
112
|
+
public short dmTTOption;
|
|
113
|
+
public short dmCollate;
|
|
114
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
|
115
|
+
public string dmFormName;
|
|
116
|
+
public short dmLogPixels;
|
|
117
|
+
public int dmBitsPerPel;
|
|
118
|
+
public int dmPelsWidth;
|
|
119
|
+
public int dmPelsHeight;
|
|
120
|
+
public int dmDisplayFlags;
|
|
121
|
+
public int dmDisplayFrequency;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public static string SetResolution(int width, int height) {
|
|
125
|
+
DEVMODE dm = new DEVMODE();
|
|
126
|
+
dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));
|
|
127
|
+
if (EnumDisplaySettings(null, -1, ref dm) != 0) {
|
|
128
|
+
dm.dmPelsWidth = width;
|
|
129
|
+
dm.dmPelsHeight = height;
|
|
130
|
+
dm.dmFields = 0x80000 | 0x100000; // DM_PELSWIDTH | DM_PELSHEIGHT
|
|
131
|
+
int result = ChangeDisplaySettings(ref dm, 0);
|
|
132
|
+
if (result == 0) return "Resolution changed to " + width + "x" + height;
|
|
133
|
+
return "Failed to change resolution (code: " + result + "). Resolution may not be supported.";
|
|
134
|
+
}
|
|
135
|
+
return "Failed to enumerate display settings.";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
'@ -ErrorAction Stop
|
|
139
|
+
[DisplaySettings]::SetResolution(${match[1]}, ${match[2]})`;
|
|
140
|
+
const r = await runPowerShell(ps);
|
|
141
|
+
results.push(r.stdout || r.stderr);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (results.length === 0) {
|
|
145
|
+
return { content: [{ type: 'text', text: 'No changes specified. Provide resolution or brightness.' }], isError: true };
|
|
146
|
+
}
|
|
147
|
+
return { content: [{ type: 'text', text: results.join('\n') }] };
|
|
148
|
+
});
|
|
149
|
+
server.tool('windows_screenshot', 'Capture a screenshot of the screen, a specific window, or a region. Returns as base64 image.', {
|
|
150
|
+
target: z.enum(['screen', 'window', 'region']).default('screen').describe('What to capture'),
|
|
151
|
+
monitor: z.number().default(0).describe('Monitor index (for screen capture)'),
|
|
152
|
+
window_title: z.string().optional().describe('Window title substring (for window capture)'),
|
|
153
|
+
x: z.number().optional().describe('Region X (for region capture)'),
|
|
154
|
+
y: z.number().optional().describe('Region Y'),
|
|
155
|
+
width: z.number().optional().describe('Region width'),
|
|
156
|
+
height: z.number().optional().describe('Region height'),
|
|
157
|
+
save_path: z.string().optional().describe('Save to file instead of returning base64'),
|
|
158
|
+
}, async ({ target, monitor, window_title, x, y, width, height, save_path }) => {
|
|
159
|
+
const outPath = save_path ? resolve(save_path) : resolve(tmpdir(), `screenshot_${Date.now()}.png`);
|
|
160
|
+
let ps;
|
|
161
|
+
if (target === 'window' && window_title) {
|
|
162
|
+
ps = `
|
|
163
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
164
|
+
Add-Type -AssemblyName System.Drawing
|
|
165
|
+
Add-Type @'
|
|
166
|
+
using System;
|
|
167
|
+
using System.Runtime.InteropServices;
|
|
168
|
+
public class Win32Window {
|
|
169
|
+
[DllImport("user32.dll")] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
|
170
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
171
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
172
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
173
|
+
[DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);
|
|
174
|
+
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
175
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
176
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
177
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
178
|
+
}
|
|
179
|
+
'@
|
|
180
|
+
$target = '${window_title.replace(/'/g, "''")}'
|
|
181
|
+
$found = $null
|
|
182
|
+
[Win32Window]::EnumWindows({
|
|
183
|
+
param($hWnd, $lParam)
|
|
184
|
+
$sb = New-Object System.Text.StringBuilder 256
|
|
185
|
+
[Win32Window]::GetWindowText($hWnd, $sb, 256) | Out-Null
|
|
186
|
+
$title = $sb.ToString()
|
|
187
|
+
if ($title -like "*$target*") { $script:found = $hWnd; return $false }
|
|
188
|
+
return $true
|
|
189
|
+
}, [IntPtr]::Zero) | Out-Null
|
|
190
|
+
|
|
191
|
+
if (-not $found) { throw "Window not found: $target" }
|
|
192
|
+
|
|
193
|
+
$rect = New-Object Win32Window+RECT
|
|
194
|
+
[Win32Window]::GetWindowRect($found, [ref]$rect) | Out-Null
|
|
195
|
+
$w = $rect.Right - $rect.Left
|
|
196
|
+
$h = $rect.Bottom - $rect.Top
|
|
197
|
+
$bmp = New-Object System.Drawing.Bitmap($w, $h)
|
|
198
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
199
|
+
$g.CopyFromScreen($rect.Left, $rect.Top, 0, 0, [System.Drawing.Size]::new($w, $h))
|
|
200
|
+
$g.Dispose()
|
|
201
|
+
$bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
202
|
+
$bmp.Dispose()
|
|
203
|
+
"saved"`;
|
|
204
|
+
}
|
|
205
|
+
else if (target === 'region' && x !== undefined && y !== undefined && width && height) {
|
|
206
|
+
ps = `
|
|
207
|
+
Add-Type -AssemblyName System.Drawing
|
|
208
|
+
$bmp = New-Object System.Drawing.Bitmap(${width}, ${height})
|
|
209
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
210
|
+
$g.CopyFromScreen(${x}, ${y}, 0, 0, [System.Drawing.Size]::new(${width}, ${height}))
|
|
211
|
+
$g.Dispose()
|
|
212
|
+
$bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
213
|
+
$bmp.Dispose()
|
|
214
|
+
"saved"`;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// Full screen
|
|
218
|
+
ps = `
|
|
219
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
220
|
+
Add-Type -AssemblyName System.Drawing
|
|
221
|
+
$screen = [System.Windows.Forms.Screen]::AllScreens[${monitor}]
|
|
222
|
+
$bounds = $screen.Bounds
|
|
223
|
+
$bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
|
|
224
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
225
|
+
$g.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size)
|
|
226
|
+
$g.Dispose()
|
|
227
|
+
$bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
228
|
+
$bmp.Dispose()
|
|
229
|
+
"saved"`;
|
|
230
|
+
}
|
|
231
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
232
|
+
if (result.exitCode !== 0) {
|
|
233
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
234
|
+
}
|
|
235
|
+
if (save_path) {
|
|
236
|
+
return { content: [{ type: 'text', text: `Screenshot saved: ${outPath}` }] };
|
|
237
|
+
}
|
|
238
|
+
// Return as base64 image
|
|
239
|
+
try {
|
|
240
|
+
const data = await readFile(outPath);
|
|
241
|
+
// Clean up temp file
|
|
242
|
+
await import('node:fs/promises').then(fs => fs.unlink(outPath)).catch(() => { });
|
|
243
|
+
return {
|
|
244
|
+
content: [{
|
|
245
|
+
type: 'image',
|
|
246
|
+
data: data.toString('base64'),
|
|
247
|
+
mimeType: 'image/png',
|
|
248
|
+
}],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return { content: [{ type: 'text', text: `Screenshot captured at ${outPath} but failed to read back.` }] };
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=display.js.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_drives (#24), windows_file_search (#25)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerDriveTools(server) {
|
|
9
|
+
server.tool('windows_drives', 'List all drives/volumes with type, label, capacity, free space, and usage percentage.', {}, async () => {
|
|
10
|
+
const ps = `
|
|
11
|
+
Get-CimInstance Win32_LogicalDisk | ForEach-Object {
|
|
12
|
+
$typeMap = @{0='Unknown';1='No Root';2='Removable';3='Local';4='Network';5='CD/DVD';6='RAM Disk'}
|
|
13
|
+
[PSCustomObject]@{
|
|
14
|
+
Drive = $_.DeviceID
|
|
15
|
+
Label = $_.VolumeName
|
|
16
|
+
Type = $typeMap[[int]$_.DriveType]
|
|
17
|
+
FileSystem = $_.FileSystem
|
|
18
|
+
TotalGB = if ($_.Size) { [math]::Round($_.Size / 1GB, 1) } else { 0 }
|
|
19
|
+
FreeGB = if ($_.FreeSpace) { [math]::Round($_.FreeSpace / 1GB, 1) } else { 0 }
|
|
20
|
+
UsedPct = if ($_.Size -gt 0) { [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1) } else { 0 }
|
|
21
|
+
}
|
|
22
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
23
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
24
|
+
if (result.exitCode !== 0) {
|
|
25
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
26
|
+
}
|
|
27
|
+
const drives = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
28
|
+
const lines = drives.map((d) => {
|
|
29
|
+
const bar = d.TotalGB > 0 ? makeBar(d.UsedPct) : ' ';
|
|
30
|
+
return `${d.Drive} ${(d.Label || '').padEnd(15).slice(0, 15)} ${d.Type.padEnd(10)} ${d.FileSystem?.padEnd(5) || ' '} ${String(d.FreeGB).padStart(8)}/${String(d.TotalGB).padStart(8)} GB ${bar} ${d.UsedPct}%`;
|
|
31
|
+
});
|
|
32
|
+
const header = `Drv ${'Label'.padEnd(15)} ${'Type'.padEnd(10)} FS ${'Free'.padStart(8)}/${'Total'.padStart(8)} Usage`;
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: 'text', text: `${header}\n${'─'.repeat(90)}\n${lines.join('\n')}` }],
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
server.tool('windows_file_search', 'Search for files using Windows Search index (fast) or filesystem walk (fallback). Search by name or content.', {
|
|
38
|
+
query: z.string().describe('Search query (filename pattern or content text)'),
|
|
39
|
+
path: z.string().default('C:\\').describe('Directory to search in'),
|
|
40
|
+
type: z.enum(['name', 'content']).default('name').describe('Search by filename or file content'),
|
|
41
|
+
extension: z.string().optional().describe('File extension filter (e.g. ".txt", ".log")'),
|
|
42
|
+
limit: z.number().default(30).describe('Max results'),
|
|
43
|
+
}, async ({ query, path, type, extension, limit }) => {
|
|
44
|
+
const extFilter = extension
|
|
45
|
+
? `| Where-Object { $_.Extension -eq '${extension.replace(/'/g, "''")}' }`
|
|
46
|
+
: '';
|
|
47
|
+
let ps;
|
|
48
|
+
if (type === 'name') {
|
|
49
|
+
ps = `
|
|
50
|
+
Get-ChildItem -Path '${path.replace(/'/g, "''")}' -Recurse -File -ErrorAction SilentlyContinue ${extFilter ? '' : ''} |
|
|
51
|
+
Where-Object { $_.Name -like '*${query.replace(/'/g, "''")}*' } ${extFilter} |
|
|
52
|
+
Select-Object -First ${limit} |
|
|
53
|
+
ForEach-Object {
|
|
54
|
+
[PSCustomObject]@{
|
|
55
|
+
Path = $_.FullName
|
|
56
|
+
Size = $_.Length
|
|
57
|
+
Modified = $_.LastWriteTime.ToString('yyyy-MM-dd HH:mm')
|
|
58
|
+
Extension = $_.Extension
|
|
59
|
+
}
|
|
60
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
ps = `
|
|
64
|
+
Get-ChildItem -Path '${path.replace(/'/g, "''")}' -Recurse -File -ErrorAction SilentlyContinue ${extFilter} |
|
|
65
|
+
Select-String -Pattern '${query.replace(/'/g, "''")}' -List -ErrorAction SilentlyContinue |
|
|
66
|
+
Select-Object -First ${limit} |
|
|
67
|
+
ForEach-Object {
|
|
68
|
+
[PSCustomObject]@{
|
|
69
|
+
Path = $_.Path
|
|
70
|
+
Line = $_.LineNumber
|
|
71
|
+
Match = $_.Line.Trim().Substring(0, [math]::Min($_.Line.Trim().Length, 120))
|
|
72
|
+
}
|
|
73
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
74
|
+
}
|
|
75
|
+
const result = await runPowerShell(ps, { timeout: 60000 });
|
|
76
|
+
if (result.exitCode !== 0) {
|
|
77
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
78
|
+
}
|
|
79
|
+
if (!result.stdout) {
|
|
80
|
+
return { content: [{ type: 'text', text: 'No results found.' }] };
|
|
81
|
+
}
|
|
82
|
+
const results = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
83
|
+
let text;
|
|
84
|
+
if (type === 'name') {
|
|
85
|
+
text = results.map((r) => `${String(r.Size).padStart(10)} ${r.Modified} ${r.Path}`).join('\n');
|
|
86
|
+
text = `${'Size'.padStart(10)} ${'Modified'.padEnd(16)} Path\n${'─'.repeat(80)}\n${text}`;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
text = results.map((r) => `${r.Path}:${r.Line}: ${r.Match}`).join('\n');
|
|
90
|
+
}
|
|
91
|
+
return { content: [{ type: 'text', text: `${results.length} result(s):\n\n${text}` }] };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function makeBar(pct) {
|
|
95
|
+
const filled = Math.round(pct / 10);
|
|
96
|
+
return '[' + '█'.repeat(filled) + '░'.repeat(10 - filled) + ']';
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=drives.js.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_env_get (#31), windows_env_set (#32)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerEnvironmentTools(server) {
|
|
9
|
+
server.tool('windows_env_get', 'Get environment variables. Can retrieve a specific variable or list all (user, system, or both).', {
|
|
10
|
+
name: z.string().optional().describe('Variable name (omit to list all)'),
|
|
11
|
+
scope: z.enum(['user', 'system', 'both']).default('both').describe('Variable scope'),
|
|
12
|
+
}, async ({ name, scope }) => {
|
|
13
|
+
if (name) {
|
|
14
|
+
if (name.toUpperCase() === 'PATH') {
|
|
15
|
+
const ps = `
|
|
16
|
+
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
17
|
+
$sysPath = [Environment]::GetEnvironmentVariable('PATH', 'Machine')
|
|
18
|
+
[PSCustomObject]@{
|
|
19
|
+
UserPATH = ($userPath -split ';' | Where-Object { $_ })
|
|
20
|
+
SystemPATH = ($sysPath -split ';' | Where-Object { $_ })
|
|
21
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
22
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
23
|
+
if (result.exitCode !== 0) {
|
|
24
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
25
|
+
}
|
|
26
|
+
const p = JSON.parse(result.stdout);
|
|
27
|
+
const userPaths = Array.isArray(p.UserPATH) ? p.UserPATH : [p.UserPATH].filter(Boolean);
|
|
28
|
+
const sysPaths = Array.isArray(p.SystemPATH) ? p.SystemPATH : [p.SystemPATH].filter(Boolean);
|
|
29
|
+
return {
|
|
30
|
+
content: [{
|
|
31
|
+
type: 'text',
|
|
32
|
+
text: `System PATH (${sysPaths.length} entries):\n${sysPaths.map((p) => ` ${p}`).join('\n')}\n\nUser PATH (${userPaths.length} entries):\n${userPaths.map((p) => ` ${p}`).join('\n')}`,
|
|
33
|
+
}],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const ps = `
|
|
37
|
+
$user = [Environment]::GetEnvironmentVariable('${name.replace(/'/g, "''")}', 'User')
|
|
38
|
+
$sys = [Environment]::GetEnvironmentVariable('${name.replace(/'/g, "''")}', 'Machine')
|
|
39
|
+
$proc = [Environment]::GetEnvironmentVariable('${name.replace(/'/g, "''")}', 'Process')
|
|
40
|
+
[PSCustomObject]@{ Name = '${name.replace(/'/g, "''")}'; User = $user; System = $sys; Process = $proc } | ConvertTo-Json -Compress`;
|
|
41
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
42
|
+
if (result.exitCode !== 0) {
|
|
43
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
44
|
+
}
|
|
45
|
+
const v = JSON.parse(result.stdout);
|
|
46
|
+
const lines = [`${v.Name}:`];
|
|
47
|
+
if (v.System !== null)
|
|
48
|
+
lines.push(` System: ${v.System}`);
|
|
49
|
+
if (v.User !== null)
|
|
50
|
+
lines.push(` User: ${v.User}`);
|
|
51
|
+
if (v.Process !== null && v.Process !== v.System && v.Process !== v.User)
|
|
52
|
+
lines.push(` Process: ${v.Process}`);
|
|
53
|
+
if (v.System === null && v.User === null)
|
|
54
|
+
lines.push(' (not set)');
|
|
55
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
56
|
+
}
|
|
57
|
+
// List all
|
|
58
|
+
const scopes = scope === 'both' ? ['User', 'Machine'] : [scope === 'user' ? 'User' : 'Machine'];
|
|
59
|
+
const parts = [];
|
|
60
|
+
for (const s of scopes) {
|
|
61
|
+
const ps = `[Environment]::GetEnvironmentVariables('${s}').GetEnumerator() | Sort-Object Name | ForEach-Object { [PSCustomObject]@{ Name = $_.Name; Value = $_.Value } } | ConvertTo-Json -Depth 3 -Compress`;
|
|
62
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
63
|
+
if (result.exitCode !== 0)
|
|
64
|
+
continue;
|
|
65
|
+
if (!result.stdout)
|
|
66
|
+
continue;
|
|
67
|
+
const vars = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
68
|
+
const lines = vars.map((v) => ` ${v.Name.padEnd(30)} ${(v.Value || '').slice(0, 60)}`);
|
|
69
|
+
parts.push(`${s} Variables (${vars.length}):\n${lines.join('\n')}`);
|
|
70
|
+
}
|
|
71
|
+
return { content: [{ type: 'text', text: parts.join('\n\n') }] };
|
|
72
|
+
});
|
|
73
|
+
server.tool('windows_env_set', 'Set or remove a persistent environment variable (user or system scope).', {
|
|
74
|
+
name: z.string().describe('Variable name'),
|
|
75
|
+
value: z.string().optional().describe('Value to set (omit with action=remove to delete)'),
|
|
76
|
+
scope: z.enum(['user', 'system']).default('user').describe('Variable scope'),
|
|
77
|
+
action: z.enum(['set', 'remove', 'append_path', 'prepend_path']).default('set').describe('Action'),
|
|
78
|
+
}, async ({ name, value, scope, action }) => {
|
|
79
|
+
const target = scope === 'user' ? 'User' : 'Machine';
|
|
80
|
+
let ps;
|
|
81
|
+
switch (action) {
|
|
82
|
+
case 'set':
|
|
83
|
+
if (!value) {
|
|
84
|
+
return { content: [{ type: 'text', text: 'Set requires a value.' }], isError: true };
|
|
85
|
+
}
|
|
86
|
+
ps = `[Environment]::SetEnvironmentVariable('${name.replace(/'/g, "''")}', '${value.replace(/'/g, "''")}', '${target}'); "Set ${name}=${value} (${target})"`;
|
|
87
|
+
break;
|
|
88
|
+
case 'remove':
|
|
89
|
+
ps = `[Environment]::SetEnvironmentVariable('${name.replace(/'/g, "''")}', $null, '${target}'); "Removed ${name} (${target})"`;
|
|
90
|
+
break;
|
|
91
|
+
case 'append_path':
|
|
92
|
+
if (!value) {
|
|
93
|
+
return { content: [{ type: 'text', text: 'append_path requires a value.' }], isError: true };
|
|
94
|
+
}
|
|
95
|
+
ps = `
|
|
96
|
+
$current = [Environment]::GetEnvironmentVariable('PATH', '${target}')
|
|
97
|
+
$entries = $current -split ';' | Where-Object { $_ }
|
|
98
|
+
if ('${value.replace(/'/g, "''")}' -notin $entries) {
|
|
99
|
+
$new = ($entries + '${value.replace(/'/g, "''")}') -join ';'
|
|
100
|
+
[Environment]::SetEnvironmentVariable('PATH', $new, '${target}')
|
|
101
|
+
"Appended '${value}' to ${target} PATH"
|
|
102
|
+
} else {
|
|
103
|
+
"'${value}' already in ${target} PATH"
|
|
104
|
+
}`;
|
|
105
|
+
break;
|
|
106
|
+
case 'prepend_path':
|
|
107
|
+
if (!value) {
|
|
108
|
+
return { content: [{ type: 'text', text: 'prepend_path requires a value.' }], isError: true };
|
|
109
|
+
}
|
|
110
|
+
ps = `
|
|
111
|
+
$current = [Environment]::GetEnvironmentVariable('PATH', '${target}')
|
|
112
|
+
$entries = $current -split ';' | Where-Object { $_ }
|
|
113
|
+
if ('${value.replace(/'/g, "''")}' -notin $entries) {
|
|
114
|
+
$new = ('${value.replace(/'/g, "''")}' + ';' + ($entries -join ';'))
|
|
115
|
+
[Environment]::SetEnvironmentVariable('PATH', $new, '${target}')
|
|
116
|
+
"Prepended '${value}' to ${target} PATH"
|
|
117
|
+
} else {
|
|
118
|
+
"'${value}' already in ${target} PATH"
|
|
119
|
+
}`;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: 'text', text: result.stdout || result.stderr }],
|
|
125
|
+
isError: result.exitCode !== 0,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=environment.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_execute — Execute shell commands (#1)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runShell } from '../shell.js';
|
|
8
|
+
export function registerExecuteTools(server) {
|
|
9
|
+
server.tool('windows_execute', 'Execute a shell command (PowerShell, cmd, or bash). Returns stdout, stderr, and exit code.', {
|
|
10
|
+
command: z.string().describe('The command to execute'),
|
|
11
|
+
shell: z.enum(['pwsh', 'cmd', 'bash']).default('pwsh').describe('Shell to use'),
|
|
12
|
+
timeout: z.number().optional().describe('Timeout in milliseconds (default 30000)'),
|
|
13
|
+
cwd: z.string().optional().describe('Working directory'),
|
|
14
|
+
}, async ({ command, shell, timeout, cwd }) => {
|
|
15
|
+
const result = await runShell(command, { shell, timeout, cwd });
|
|
16
|
+
const parts = [];
|
|
17
|
+
if (result.stdout)
|
|
18
|
+
parts.push(result.stdout);
|
|
19
|
+
if (result.stderr)
|
|
20
|
+
parts.push(`[stderr]\n${result.stderr}`);
|
|
21
|
+
parts.push(`[exit code: ${result.exitCode}]`);
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: 'text', text: parts.join('\n\n') }],
|
|
24
|
+
isError: result.exitCode !== 0,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=execute.js.map
|