@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,211 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_window_list (#14), windows_window_control (#15)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { runPowerShell } from '../shell.js';
|
|
10
|
+
|
|
11
|
+
const WIN32_TYPES = `
|
|
12
|
+
Add-Type @'
|
|
13
|
+
using System;
|
|
14
|
+
using System.Text;
|
|
15
|
+
using System.Collections.Generic;
|
|
16
|
+
using System.Runtime.InteropServices;
|
|
17
|
+
|
|
18
|
+
public class WindowManager {
|
|
19
|
+
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
20
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
21
|
+
[DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
|
|
22
|
+
[DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd);
|
|
23
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
24
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
|
25
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
26
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
27
|
+
[DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
|
|
28
|
+
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
|
29
|
+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
|
|
30
|
+
[DllImport("user32.dll")] public static extern bool IsZoomed(IntPtr hWnd);
|
|
31
|
+
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
|
|
32
|
+
|
|
33
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
34
|
+
|
|
35
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
36
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
37
|
+
|
|
38
|
+
public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
|
|
39
|
+
public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
|
|
40
|
+
public const uint SWP_NOMOVE = 0x0002;
|
|
41
|
+
public const uint SWP_NOSIZE = 0x0001;
|
|
42
|
+
public const uint WM_CLOSE = 0x0010;
|
|
43
|
+
}
|
|
44
|
+
'@ -ErrorAction SilentlyContinue
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export function registerWindowTools(server: McpServer): void {
|
|
48
|
+
server.tool(
|
|
49
|
+
'windows_window_list',
|
|
50
|
+
'List all visible windows with title, PID, process name, position, size, and state.',
|
|
51
|
+
{
|
|
52
|
+
filter: z.string().optional().describe('Filter by window title (substring)'),
|
|
53
|
+
},
|
|
54
|
+
async ({ filter }) => {
|
|
55
|
+
const filterClause = filter
|
|
56
|
+
? `| Where-Object { $_.Title -like '*${filter.replace(/'/g, "''")}*' }`
|
|
57
|
+
: '';
|
|
58
|
+
|
|
59
|
+
const ps = `
|
|
60
|
+
${WIN32_TYPES}
|
|
61
|
+
$windows = [System.Collections.Generic.List[PSObject]]::new()
|
|
62
|
+
$zOrder = 0
|
|
63
|
+
[WindowManager]::EnumWindows({
|
|
64
|
+
param($hWnd, $lParam)
|
|
65
|
+
if (-not [WindowManager]::IsWindowVisible($hWnd)) { return $true }
|
|
66
|
+
$len = [WindowManager]::GetWindowTextLength($hWnd)
|
|
67
|
+
if ($len -eq 0) { return $true }
|
|
68
|
+
$sb = New-Object System.Text.StringBuilder($len + 1)
|
|
69
|
+
[WindowManager]::GetWindowText($hWnd, $sb, $sb.Capacity) | Out-Null
|
|
70
|
+
$title = $sb.ToString()
|
|
71
|
+
if (-not $title) { return $true }
|
|
72
|
+
|
|
73
|
+
$pid = [uint32]0
|
|
74
|
+
[WindowManager]::GetWindowThreadProcessId($hWnd, [ref]$pid) | Out-Null
|
|
75
|
+
$proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
|
|
76
|
+
|
|
77
|
+
$rect = New-Object WindowManager+RECT
|
|
78
|
+
[WindowManager]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
|
|
79
|
+
|
|
80
|
+
$state = 'Normal'
|
|
81
|
+
if ([WindowManager]::IsIconic($hWnd)) { $state = 'Minimized' }
|
|
82
|
+
elseif ([WindowManager]::IsZoomed($hWnd)) { $state = 'Maximized' }
|
|
83
|
+
|
|
84
|
+
$script:windows.Add([PSCustomObject]@{
|
|
85
|
+
ZOrder = $script:zOrder++
|
|
86
|
+
Title = $title
|
|
87
|
+
PID = $pid
|
|
88
|
+
Process = if ($proc) { $proc.ProcessName } else { '?' }
|
|
89
|
+
X = $rect.Left
|
|
90
|
+
Y = $rect.Top
|
|
91
|
+
Width = $rect.Right - $rect.Left
|
|
92
|
+
Height = $rect.Bottom - $rect.Top
|
|
93
|
+
State = $state
|
|
94
|
+
})
|
|
95
|
+
return $true
|
|
96
|
+
}, [IntPtr]::Zero) | Out-Null
|
|
97
|
+
|
|
98
|
+
$windows ${filterClause} | ConvertTo-Json -Depth 3 -Compress`;
|
|
99
|
+
|
|
100
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
101
|
+
if (result.exitCode !== 0) {
|
|
102
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!result.stdout) {
|
|
106
|
+
return { content: [{ type: 'text', text: 'No visible windows found.' }] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const windows = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
110
|
+
const lines = windows.map((w: { ZOrder: number; Title: string; PID: number; Process: string; X: number; Y: number; Width: number; Height: number; State: string }) => {
|
|
111
|
+
const stateIcon = w.State === 'Minimized' ? '[-]' : w.State === 'Maximized' ? '[+]' : '[ ]';
|
|
112
|
+
return `${stateIcon} ${String(w.PID).padStart(6)} ${w.Process.padEnd(20).slice(0, 20)} ${String(w.Width).padStart(5)}x${String(w.Height).padEnd(5)} (${w.X},${w.Y}) ${w.Title.slice(0, 60)}`;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const header = `Sta ${'PID'.padStart(6)} ${'Process'.padEnd(20)} ${'Size'.padStart(11)} Pos Title`;
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: 'text', text: `${header}\n${'─'.repeat(110)}\n${lines.join('\n')}\n\n${windows.length} windows` }],
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
server.tool(
|
|
123
|
+
'windows_window_control',
|
|
124
|
+
'Move, resize, minimize, maximize, restore, close, or focus a window.',
|
|
125
|
+
{
|
|
126
|
+
title: z.string().optional().describe('Window title (substring match)'),
|
|
127
|
+
pid: z.number().optional().describe('Process ID'),
|
|
128
|
+
action: z.enum(['minimize', 'maximize', 'restore', 'close', 'focus', 'move', 'resize', 'topmost']).describe('Action'),
|
|
129
|
+
x: z.number().optional().describe('X position (for move)'),
|
|
130
|
+
y: z.number().optional().describe('Y position (for move)'),
|
|
131
|
+
width: z.number().optional().describe('Width (for resize)'),
|
|
132
|
+
height: z.number().optional().describe('Height (for resize)'),
|
|
133
|
+
topmost: z.boolean().optional().describe('Set always-on-top (for topmost action)'),
|
|
134
|
+
},
|
|
135
|
+
async ({ title, pid, action, x, y, width, height, topmost }) => {
|
|
136
|
+
if (!title && !pid) {
|
|
137
|
+
return { content: [{ type: 'text', text: 'Provide either title or pid.' }], isError: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const findWindow = title
|
|
141
|
+
? `
|
|
142
|
+
$target = '${title.replace(/'/g, "''")}'
|
|
143
|
+
$hWnd = [IntPtr]::Zero
|
|
144
|
+
[WindowManager]::EnumWindows({
|
|
145
|
+
param($h, $l)
|
|
146
|
+
$sb = New-Object System.Text.StringBuilder 256
|
|
147
|
+
[WindowManager]::GetWindowText($h, $sb, 256) | Out-Null
|
|
148
|
+
if ($sb.ToString() -like "*$target*" -and [WindowManager]::IsWindowVisible($h)) {
|
|
149
|
+
$script:hWnd = $h; return $false
|
|
150
|
+
}
|
|
151
|
+
return $true
|
|
152
|
+
}, [IntPtr]::Zero) | Out-Null
|
|
153
|
+
if ($hWnd -eq [IntPtr]::Zero) { throw "Window not found: $target" }`
|
|
154
|
+
: `
|
|
155
|
+
$proc = Get-Process -Id ${pid} -ErrorAction Stop
|
|
156
|
+
$hWnd = $proc.MainWindowHandle
|
|
157
|
+
if ($hWnd -eq [IntPtr]::Zero) { throw "Process ${pid} has no visible window" }`;
|
|
158
|
+
|
|
159
|
+
let actionCode: string;
|
|
160
|
+
switch (action) {
|
|
161
|
+
case 'minimize':
|
|
162
|
+
actionCode = `[WindowManager]::ShowWindow($hWnd, 6) | Out-Null; "Minimized"`;
|
|
163
|
+
break;
|
|
164
|
+
case 'maximize':
|
|
165
|
+
actionCode = `[WindowManager]::ShowWindow($hWnd, 3) | Out-Null; "Maximized"`;
|
|
166
|
+
break;
|
|
167
|
+
case 'restore':
|
|
168
|
+
actionCode = `[WindowManager]::ShowWindow($hWnd, 9) | Out-Null; "Restored"`;
|
|
169
|
+
break;
|
|
170
|
+
case 'close':
|
|
171
|
+
actionCode = `[WindowManager]::PostMessage($hWnd, [WindowManager]::WM_CLOSE, [IntPtr]::Zero, [IntPtr]::Zero) | Out-Null; "Close message sent"`;
|
|
172
|
+
break;
|
|
173
|
+
case 'focus':
|
|
174
|
+
actionCode = `[WindowManager]::ShowWindow($hWnd, 9) | Out-Null; [WindowManager]::SetForegroundWindow($hWnd) | Out-Null; "Focused"`;
|
|
175
|
+
break;
|
|
176
|
+
case 'move':
|
|
177
|
+
if (x === undefined || y === undefined) {
|
|
178
|
+
return { content: [{ type: 'text', text: 'Move requires x and y.' }], isError: true };
|
|
179
|
+
}
|
|
180
|
+
actionCode = `
|
|
181
|
+
$rect = New-Object WindowManager+RECT
|
|
182
|
+
[WindowManager]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
|
|
183
|
+
$w = $rect.Right - $rect.Left; $h = $rect.Bottom - $rect.Top
|
|
184
|
+
[WindowManager]::MoveWindow($hWnd, ${x}, ${y}, $w, $h, $true) | Out-Null
|
|
185
|
+
"Moved to (${x}, ${y})"`;
|
|
186
|
+
break;
|
|
187
|
+
case 'resize':
|
|
188
|
+
if (!width || !height) {
|
|
189
|
+
return { content: [{ type: 'text', text: 'Resize requires width and height.' }], isError: true };
|
|
190
|
+
}
|
|
191
|
+
actionCode = `
|
|
192
|
+
$rect = New-Object WindowManager+RECT
|
|
193
|
+
[WindowManager]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
|
|
194
|
+
[WindowManager]::MoveWindow($hWnd, $rect.Left, $rect.Top, ${width}, ${height}, $true) | Out-Null
|
|
195
|
+
"Resized to ${width}x${height}"`;
|
|
196
|
+
break;
|
|
197
|
+
case 'topmost':
|
|
198
|
+
const insertAfter = topmost !== false ? '[WindowManager]::HWND_TOPMOST' : '[WindowManager]::HWND_NOTOPMOST';
|
|
199
|
+
actionCode = `[WindowManager]::SetWindowPos($hWnd, ${insertAfter}, 0, 0, 0, 0, [WindowManager]::SWP_NOMOVE -bor [WindowManager]::SWP_NOSIZE) | Out-Null; "Topmost: ${topmost !== false}"`;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const ps = `${WIN32_TYPES}\n${findWindow}\n${actionCode}`;
|
|
204
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: 'text', text: result.stdout || result.stderr }],
|
|
207
|
+
isError: result.exitCode !== 0,
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_winget_search (#69), windows_winget_manage (#70), windows_winget_export (#71)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { runShell } from '../shell.js';
|
|
10
|
+
|
|
11
|
+
export function registerWingetTools(server: McpServer): void {
|
|
12
|
+
server.tool(
|
|
13
|
+
'windows_winget_search',
|
|
14
|
+
'Search for packages in the winget repository.',
|
|
15
|
+
{
|
|
16
|
+
query: z.string().describe('Search query (name or ID)'),
|
|
17
|
+
source: z.enum(['winget', 'msstore', 'all']).default('all').describe('Package source'),
|
|
18
|
+
limit: z.number().default(20).describe('Max results'),
|
|
19
|
+
},
|
|
20
|
+
async ({ query, source, limit }) => {
|
|
21
|
+
const sourceArg = source !== 'all' ? `--source ${source}` : '';
|
|
22
|
+
const cmd = `winget search "${query}" ${sourceArg} --count ${limit} --accept-source-agreements --disable-interactivity`;
|
|
23
|
+
const result = await runShell(cmd, { shell: 'cmd', timeout: 30000 });
|
|
24
|
+
const clean = result.stdout.replace(/\0/g, '').trim();
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: 'text', text: clean || 'No packages found.' }],
|
|
27
|
+
isError: result.exitCode !== 0 && !clean,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
server.tool(
|
|
33
|
+
'windows_winget_manage',
|
|
34
|
+
'Install, upgrade, or uninstall packages via winget. Also list installed or upgradable packages.',
|
|
35
|
+
{
|
|
36
|
+
action: z.enum(['install', 'upgrade', 'uninstall', 'list', 'upgradable', 'upgrade_all']).describe('Action'),
|
|
37
|
+
id: z.string().optional().describe('Package ID (e.g. "Microsoft.VisualStudioCode")'),
|
|
38
|
+
version: z.string().optional().describe('Specific version to install'),
|
|
39
|
+
},
|
|
40
|
+
async ({ action, id, version }) => {
|
|
41
|
+
let cmd: string;
|
|
42
|
+
|
|
43
|
+
switch (action) {
|
|
44
|
+
case 'install':
|
|
45
|
+
if (!id) return { content: [{ type: 'text', text: 'install requires id.' }], isError: true };
|
|
46
|
+
cmd = `winget install --id "${id}" ${version ? `--version "${version}"` : ''} --accept-package-agreements --accept-source-agreements --disable-interactivity`;
|
|
47
|
+
break;
|
|
48
|
+
case 'upgrade':
|
|
49
|
+
if (!id) return { content: [{ type: 'text', text: 'upgrade requires id.' }], isError: true };
|
|
50
|
+
cmd = `winget upgrade --id "${id}" --accept-package-agreements --accept-source-agreements --disable-interactivity`;
|
|
51
|
+
break;
|
|
52
|
+
case 'upgrade_all':
|
|
53
|
+
cmd = `winget upgrade --all --accept-package-agreements --accept-source-agreements --disable-interactivity`;
|
|
54
|
+
break;
|
|
55
|
+
case 'uninstall':
|
|
56
|
+
if (!id) return { content: [{ type: 'text', text: 'uninstall requires id.' }], isError: true };
|
|
57
|
+
cmd = `winget uninstall --id "${id}" --disable-interactivity`;
|
|
58
|
+
break;
|
|
59
|
+
case 'list':
|
|
60
|
+
cmd = `winget list --accept-source-agreements --disable-interactivity`;
|
|
61
|
+
break;
|
|
62
|
+
case 'upgradable':
|
|
63
|
+
cmd = `winget upgrade --accept-source-agreements --disable-interactivity`;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const timeout = action === 'install' || action === 'upgrade' || action === 'upgrade_all' ? 300000 : 30000;
|
|
68
|
+
const result = await runShell(cmd, { shell: 'cmd', timeout });
|
|
69
|
+
const clean = result.stdout.replace(/\0/g, '').trim();
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: 'text', text: clean || result.stderr || `${action} completed.` }],
|
|
73
|
+
isError: result.exitCode !== 0 && !clean,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
server.tool(
|
|
79
|
+
'windows_winget_export',
|
|
80
|
+
'Export installed packages to JSON or import from a JSON file.',
|
|
81
|
+
{
|
|
82
|
+
action: z.enum(['export', 'import']).describe('Action'),
|
|
83
|
+
path: z.string().describe('File path for export/import JSON'),
|
|
84
|
+
},
|
|
85
|
+
async ({ action, path }) => {
|
|
86
|
+
const cmd = action === 'export'
|
|
87
|
+
? `winget export -o "${path}" --accept-source-agreements --disable-interactivity`
|
|
88
|
+
: `winget import -i "${path}" --accept-package-agreements --accept-source-agreements --disable-interactivity`;
|
|
89
|
+
|
|
90
|
+
const timeout = action === 'import' ? 600000 : 30000;
|
|
91
|
+
const result = await runShell(cmd, { shell: 'cmd', timeout });
|
|
92
|
+
const clean = result.stdout.replace(/\0/g, '').trim();
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: 'text', text: clean || `${action} completed: ${path}` }],
|
|
96
|
+
isError: result.exitCode !== 0 && !clean,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
}
|
package/src/tools/wsl.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_wsl_list (#66), windows_wsl_manage (#67), windows_wsl_exec (#68)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { runShell, runPowerShell } from '../shell.js';
|
|
10
|
+
|
|
11
|
+
export function registerWslTools(server: McpServer): void {
|
|
12
|
+
server.tool(
|
|
13
|
+
'windows_wsl_list',
|
|
14
|
+
'List installed WSL distributions with status, version, and default flag.',
|
|
15
|
+
{},
|
|
16
|
+
async () => {
|
|
17
|
+
// wsl.exe outputs UTF-16; use PowerShell to decode properly
|
|
18
|
+
const ps = `$out = wsl --list --verbose 2>&1; if ($LASTEXITCODE -ne 0) { "WSL_ERROR:$out" } else { $out }`;
|
|
19
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
20
|
+
const clean = result.stdout.replace(/\0/g, '').trim();
|
|
21
|
+
if (clean.startsWith('WSL_ERROR:') || result.exitCode !== 0) {
|
|
22
|
+
const msg = clean.replace('WSL_ERROR:', '').trim();
|
|
23
|
+
return { content: [{ type: 'text', text: msg || 'WSL is not installed or not available. Install with: wsl --install' }] };
|
|
24
|
+
}
|
|
25
|
+
return { content: [{ type: 'text', text: clean || 'No WSL distributions installed.' }] };
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
server.tool(
|
|
30
|
+
'windows_wsl_manage',
|
|
31
|
+
'Manage WSL: start, stop, shutdown, set default, export, import, or remove distros.',
|
|
32
|
+
{
|
|
33
|
+
action: z.enum(['shutdown', 'terminate', 'set_default', 'set_version', 'export', 'import', 'unregister']).describe('Action'),
|
|
34
|
+
distro: z.string().optional().describe('Distribution name'),
|
|
35
|
+
version: z.number().optional().describe('WSL version 1 or 2 (for set_version)'),
|
|
36
|
+
path: z.string().optional().describe('File path (for export/import)'),
|
|
37
|
+
},
|
|
38
|
+
async ({ action, distro, version, path }) => {
|
|
39
|
+
let cmd: string;
|
|
40
|
+
|
|
41
|
+
switch (action) {
|
|
42
|
+
case 'shutdown':
|
|
43
|
+
cmd = 'wsl --shutdown';
|
|
44
|
+
break;
|
|
45
|
+
case 'terminate':
|
|
46
|
+
if (!distro) return { content: [{ type: 'text', text: 'terminate requires distro.' }], isError: true };
|
|
47
|
+
cmd = `wsl --terminate ${distro}`;
|
|
48
|
+
break;
|
|
49
|
+
case 'set_default':
|
|
50
|
+
if (!distro) return { content: [{ type: 'text', text: 'set_default requires distro.' }], isError: true };
|
|
51
|
+
cmd = `wsl --set-default ${distro}`;
|
|
52
|
+
break;
|
|
53
|
+
case 'set_version':
|
|
54
|
+
if (!distro || !version) return { content: [{ type: 'text', text: 'set_version requires distro and version.' }], isError: true };
|
|
55
|
+
cmd = `wsl --set-version ${distro} ${version}`;
|
|
56
|
+
break;
|
|
57
|
+
case 'export':
|
|
58
|
+
if (!distro || !path) return { content: [{ type: 'text', text: 'export requires distro and path.' }], isError: true };
|
|
59
|
+
cmd = `wsl --export ${distro} "${path}"`;
|
|
60
|
+
break;
|
|
61
|
+
case 'import':
|
|
62
|
+
if (!distro || !path) return { content: [{ type: 'text', text: 'import requires distro and path.' }], isError: true };
|
|
63
|
+
cmd = `wsl --import ${distro} "C:\\WSL\\${distro}" "${path}"`;
|
|
64
|
+
break;
|
|
65
|
+
case 'unregister':
|
|
66
|
+
if (!distro) return { content: [{ type: 'text', text: 'unregister requires distro.' }], isError: true };
|
|
67
|
+
cmd = `wsl --unregister ${distro}`;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = await runShell(cmd, { shell: 'cmd', timeout: 60000 });
|
|
72
|
+
const clean = (result.stdout + '\n' + result.stderr).replace(/\0/g, '').trim();
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: 'text', text: clean || `${action} completed.` }],
|
|
75
|
+
isError: result.exitCode !== 0,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
server.tool(
|
|
81
|
+
'windows_wsl_exec',
|
|
82
|
+
'Execute a command inside a WSL distribution. Returns stdout, stderr, exit code.',
|
|
83
|
+
{
|
|
84
|
+
command: z.string().describe('Command to execute'),
|
|
85
|
+
distro: z.string().optional().describe('Distribution name (default distro if omitted)'),
|
|
86
|
+
user: z.string().optional().describe('Run as user'),
|
|
87
|
+
cwd: z.string().optional().describe('Working directory inside WSL'),
|
|
88
|
+
timeout: z.number().default(30000).describe('Timeout in ms'),
|
|
89
|
+
},
|
|
90
|
+
async ({ command, distro, user, cwd, timeout }) => {
|
|
91
|
+
const parts = ['wsl'];
|
|
92
|
+
if (distro) parts.push('-d', distro);
|
|
93
|
+
if (user) parts.push('-u', user);
|
|
94
|
+
if (cwd) parts.push('--cd', cwd);
|
|
95
|
+
parts.push('--', 'bash', '-c', `"${command.replace(/"/g, '\\"')}"`);
|
|
96
|
+
|
|
97
|
+
const result = await runShell(parts.join(' '), { shell: 'cmd', timeout });
|
|
98
|
+
const clean = result.stdout.replace(/\0/g, '').trim();
|
|
99
|
+
const cleanErr = result.stderr.replace(/\0/g, '').trim();
|
|
100
|
+
|
|
101
|
+
const output: string[] = [];
|
|
102
|
+
if (clean) output.push(clean);
|
|
103
|
+
if (cleanErr) output.push(`[stderr]\n${cleanErr}`);
|
|
104
|
+
output.push(`[exit code: ${result.exitCode}]`);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: 'text', text: output.join('\n\n') }],
|
|
108
|
+
isError: result.exitCode !== 0,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|