feat: implement v1.2 desktop automation tools (8 new tools)
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled

- tools/display.ts: windows_display_get, windows_display_set, windows_screenshot (#9-11)
- tools/window.ts: windows_window_list, windows_window_control (#14, #15)
- tools/clipboard.ts: windows_clipboard_get, windows_clipboard_set (#16, #17)
- tools/notification.ts: windows_notification_send (#20)

Uses Win32 P/Invoke for window management and System.Drawing for screenshots.
Clipboard uses STA PowerShell subprocess for COM threading.

Total: 31 tools registered (v1.0 + v1.1 + v1.2 complete)

Authored-by: Moko Consulting
This commit is contained in:
Jonathan Miller
2026-05-25 21:28:46 -05:00
parent 7cf3dbe420
commit 2b29940d8e
5 changed files with 694 additions and 0 deletions
+10
View File
@@ -19,6 +19,10 @@ import { registerAudioAppTools } from './tools/audio_apps.js';
import { registerPowerTools } from './tools/power.js';
import { registerNetworkTools } from './tools/network.js';
import { registerDriveTools } from './tools/drives.js';
import { registerDisplayTools } from './tools/display.js';
import { registerWindowTools } from './tools/window.js';
import { registerClipboardTools } from './tools/clipboard.js';
import { registerNotificationTools } from './tools/notification.js';
const server = new McpServer({
name: 'mcp_windows',
@@ -41,6 +45,12 @@ registerPowerTools(server);
registerNetworkTools(server);
registerDriveTools(server);
// v1.2 — Desktop Automation
registerDisplayTools(server);
registerWindowTools(server);
registerClipboardTools(server);
registerNotificationTools(server);
async function main(): Promise<void> {
const transport = new StdioServerTransport();
await server.connect(transport);
+138
View File
@@ -0,0 +1,138 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tools: windows_clipboard_get (#16), windows_clipboard_set (#17)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerClipboardTools(server: McpServer): void {
server.tool(
'windows_clipboard_get',
'Read clipboard contents: text, file list, or image (returned as base64).',
{},
async () => {
// PowerShell must run in STA mode for clipboard access
const ps = `
powershell.exe -NoProfile -STA -Command {
Add-Type -AssemblyName System.Windows.Forms
$data = [System.Windows.Forms.Clipboard]::GetDataObject()
if (-not $data) { Write-Output '{"type":"empty","content":"Clipboard is empty"}'; return }
# Check for files
if ($data.ContainsFileDropList()) {
$files = [System.Windows.Forms.Clipboard]::GetFileDropList()
$list = @()
foreach ($f in $files) { $list += $f }
$json = @{ type = 'files'; content = $list } | ConvertTo-Json -Compress
Write-Output $json
return
}
# Check for image
if ($data.ContainsImage()) {
$img = [System.Windows.Forms.Clipboard]::GetImage()
$ms = New-Object System.IO.MemoryStream
$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
$b64 = [Convert]::ToBase64String($ms.ToArray())
$ms.Dispose()
$img.Dispose()
$json = @{ type = 'image'; content = $b64 } | ConvertTo-Json -Compress
Write-Output $json
return
}
# Text
if ($data.ContainsText()) {
$text = [System.Windows.Forms.Clipboard]::GetText()
$json = @{ type = 'text'; content = $text } | ConvertTo-Json -Compress
Write-Output $json
return
}
Write-Output '{"type":"unknown","content":"Clipboard contains unsupported format"}'
}`;
const result = await runPowerShell(ps, { timeout: 10000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
try {
const data = JSON.parse(result.stdout);
if (data.type === 'image') {
return {
content: [
{ type: 'text' as const, text: 'Clipboard contains an image:' },
{ type: 'image' as const, data: data.content, mimeType: 'image/png' },
],
};
}
if (data.type === 'files') {
const files = Array.isArray(data.content) ? data.content : [data.content];
return {
content: [{ type: 'text', text: `Clipboard contains ${files.length} file(s):\n${files.join('\n')}` }],
};
}
return {
content: [{ type: 'text', text: data.content || '(empty)' }],
};
} catch {
return { content: [{ type: 'text', text: result.stdout || '(empty clipboard)' }] };
}
},
);
server.tool(
'windows_clipboard_set',
'Set clipboard contents: text, file list, or clear.',
{
text: z.string().optional().describe('Text to copy to clipboard'),
files: z.array(z.string()).optional().describe('File paths to copy to clipboard'),
clear: z.boolean().optional().describe('Clear the clipboard'),
},
async ({ text, files, clear }) => {
if (clear) {
const result = await runPowerShell(`
powershell.exe -NoProfile -STA -Command {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Clipboard]::Clear()
"Clipboard cleared"
}`);
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
}
if (text) {
// Escape for PowerShell here-string
const escaped = text.replace(/'/g, "''");
const result = await runPowerShell(`
powershell.exe -NoProfile -STA -Command {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Clipboard]::SetText('${escaped}')
"Copied $([System.Windows.Forms.Clipboard]::GetText().Length) chars to clipboard"
}`);
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
}
if (files && files.length > 0) {
const pathList = files.map(f => `$fc.Add('${f.replace(/'/g, "''")}')`).join('; ');
const result = await runPowerShell(`
powershell.exe -NoProfile -STA -Command {
Add-Type -AssemblyName System.Windows.Forms
$fc = New-Object System.Collections.Specialized.StringCollection
${pathList}
[System.Windows.Forms.Clipboard]::SetFileDropList($fc)
"Copied $($fc.Count) file(s) to clipboard"
}`);
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
}
return { content: [{ type: 'text', text: 'Provide text, files, or clear.' }], isError: true };
},
);
}
+285
View File
@@ -0,0 +1,285 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tools: windows_display_get (#9), windows_display_set (#10), windows_screenshot (#11)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { tmpdir } from 'node:os';
export function registerDisplayTools(server: McpServer): void {
server.tool(
'windows_display_get',
'Get display configuration: resolution, refresh rate, scaling, multi-monitor layout, HDR status.',
{},
async () => {
const ps = `
Add-Type -AssemblyName System.Windows.Forms
$screens = [System.Windows.Forms.Screen]::AllScreens
$monitors = Get-CimInstance -Namespace root\\wmi -ClassName WmiMonitorBasicDisplayParams -ErrorAction SilentlyContinue
$videoCtrl = Get-CimInstance Win32_VideoController
$i = 0
$screens | ForEach-Object {
$s = $_
$vc = $videoCtrl | Where-Object { $_.Name -match $s.DeviceName -or $true } | Select-Object -First 1
$dpiScale = [math]::Round(($s.Bounds.Width / $s.WorkingArea.Width) * 100, 0)
# Try to get real scaling from registry
$regScale = $null
try {
$regPath = "HKCU:\\Control Panel\\Desktop\\PerMonitorSettings"
if (Test-Path $regPath) {
$regScale = (Get-ChildItem $regPath -ErrorAction SilentlyContinue | Select-Object -Index $i | Get-ItemProperty -ErrorAction SilentlyContinue).DpiValue
}
} catch {}
[PSCustomObject]@{
Index = $i
Name = $s.DeviceName
Primary = $s.Primary
Resolution = "$($s.Bounds.Width)x$($s.Bounds.Height)"
RefreshRate = if ($vc) { "$($vc.CurrentRefreshRate) Hz" } else { 'Unknown' }
Scaling = if ($regScale) { "$($regScale)%" } else { 'System default' }
Position = "($($s.Bounds.X), $($s.Bounds.Y))"
WorkArea = "$($s.WorkingArea.Width)x$($s.WorkingArea.Height)"
BitsPerPixel = if ($vc) { $vc.CurrentBitsPerPixel } else { $null }
}
$i++
} | ConvertTo-Json -Depth 3 -Compress`;
const result = await runPowerShell(ps, { timeout: 10000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
const displays = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = displays.map((d: { Index: number; Name: string; Primary: boolean; Resolution: string; RefreshRate: string; Scaling: string; Position: string; WorkArea: string }) =>
[
`Monitor ${d.Index}: ${d.Name}${d.Primary ? ' (Primary)' : ''}`,
` Resolution: ${d.Resolution} @ ${d.RefreshRate}`,
` Scaling: ${d.Scaling}`,
` Position: ${d.Position}`,
` Work area: ${d.WorkArea}`,
].join('\n'),
);
return { content: [{ type: 'text', text: lines.join('\n\n') }] };
},
);
server.tool(
'windows_display_set',
'Change display settings: resolution, brightness.',
{
resolution: z.string().optional().describe('Resolution as "WIDTHxHEIGHT" (e.g. "1920x1080")'),
brightness: z.number().min(0).max(100).optional().describe('Screen brightness 0-100 (laptops only)'),
},
async ({ resolution, brightness }) => {
const results: string[] = [];
if (brightness !== undefined) {
const ps = `
try {
$monitors = Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightnessMethods -ErrorAction Stop
$monitors | Invoke-CimMethod -MethodName WmiSetBrightness -Arguments @{Timeout=1; Brightness=${brightness}} -ErrorAction Stop
"Brightness set to ${brightness}%"
} catch {
"Error: Brightness control not available (requires laptop/integrated display). $_"
}`;
const r = await runPowerShell(ps);
results.push(r.stdout || r.stderr);
}
if (resolution) {
const match = resolution.match(/^(\d+)x(\d+)$/);
if (!match) {
results.push('Invalid resolution format. Use WIDTHxHEIGHT (e.g. 1920x1080)');
} else {
const ps = `
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class DisplaySettings {
[DllImport("user32.dll")]
public static extern int EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
[DllImport("user32.dll")]
public static extern int ChangeDisplaySettings(ref DEVMODE devMode, int flags);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DEVMODE {
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public int dmPositionX;
public int dmPositionY;
public int dmDisplayOrientation;
public int dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string dmFormName;
public short dmLogPixels;
public int dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmDisplayFrequency;
}
public static string SetResolution(int width, int height) {
DEVMODE dm = new DEVMODE();
dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));
if (EnumDisplaySettings(null, -1, ref dm) != 0) {
dm.dmPelsWidth = width;
dm.dmPelsHeight = height;
dm.dmFields = 0x80000 | 0x100000; // DM_PELSWIDTH | DM_PELSHEIGHT
int result = ChangeDisplaySettings(ref dm, 0);
if (result == 0) return "Resolution changed to " + width + "x" + height;
return "Failed to change resolution (code: " + result + "). Resolution may not be supported.";
}
return "Failed to enumerate display settings.";
}
}
'@ -ErrorAction Stop
[DisplaySettings]::SetResolution(${match[1]}, ${match[2]})`;
const r = await runPowerShell(ps);
results.push(r.stdout || r.stderr);
}
}
if (results.length === 0) {
return { content: [{ type: 'text', text: 'No changes specified. Provide resolution or brightness.' }], isError: true };
}
return { content: [{ type: 'text', text: results.join('\n') }] };
},
);
server.tool(
'windows_screenshot',
'Capture a screenshot of the screen, a specific window, or a region. Returns as base64 image.',
{
target: z.enum(['screen', 'window', 'region']).default('screen').describe('What to capture'),
monitor: z.number().default(0).describe('Monitor index (for screen capture)'),
window_title: z.string().optional().describe('Window title substring (for window capture)'),
x: z.number().optional().describe('Region X (for region capture)'),
y: z.number().optional().describe('Region Y'),
width: z.number().optional().describe('Region width'),
height: z.number().optional().describe('Region height'),
save_path: z.string().optional().describe('Save to file instead of returning base64'),
},
async ({ target, monitor, window_title, x, y, width, height, save_path }) => {
const outPath = save_path ? resolve(save_path) : resolve(tmpdir(), `screenshot_${Date.now()}.png`);
let ps: string;
if (target === 'window' && window_title) {
ps = `
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class Win32Window {
[DllImport("user32.dll")] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int Left, Top, Right, Bottom; }
}
'@
$target = '${window_title.replace(/'/g, "''")}'
$found = $null
[Win32Window]::EnumWindows({
param($hWnd, $lParam)
$sb = New-Object System.Text.StringBuilder 256
[Win32Window]::GetWindowText($hWnd, $sb, 256) | Out-Null
$title = $sb.ToString()
if ($title -like "*$target*") { $script:found = $hWnd; return $false }
return $true
}, [IntPtr]::Zero) | Out-Null
if (-not $found) { throw "Window not found: $target" }
$rect = New-Object Win32Window+RECT
[Win32Window]::GetWindowRect($found, [ref]$rect) | Out-Null
$w = $rect.Right - $rect.Left
$h = $rect.Bottom - $rect.Top
$bmp = New-Object System.Drawing.Bitmap($w, $h)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen($rect.Left, $rect.Top, 0, 0, [System.Drawing.Size]::new($w, $h))
$g.Dispose()
$bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
"saved"`;
} else if (target === 'region' && x !== undefined && y !== undefined && width && height) {
ps = `
Add-Type -AssemblyName System.Drawing
$bmp = New-Object System.Drawing.Bitmap(${width}, ${height})
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen(${x}, ${y}, 0, 0, [System.Drawing.Size]::new(${width}, ${height}))
$g.Dispose()
$bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
"saved"`;
} else {
// Full screen
ps = `
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$screen = [System.Windows.Forms.Screen]::AllScreens[${monitor}]
$bounds = $screen.Bounds
$bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size)
$g.Dispose()
$bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
"saved"`;
}
const result = await runPowerShell(ps, { timeout: 10000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
if (save_path) {
return { content: [{ type: 'text', text: `Screenshot saved: ${outPath}` }] };
}
// Return as base64 image
try {
const data = await readFile(outPath);
// Clean up temp file
await import('node:fs/promises').then(fs => fs.unlink(outPath)).catch(() => {});
return {
content: [{
type: 'image' as const,
data: data.toString('base64'),
mimeType: 'image/png',
}],
};
} catch {
return { content: [{ type: 'text', text: `Screenshot captured at ${outPath} but failed to read back.` }] };
}
},
);
}
+50
View File
@@ -0,0 +1,50 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tool: windows_notification_send (#20)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
export function registerNotificationTools(server: McpServer): void {
server.tool(
'windows_notification_send',
'Send a Windows toast notification with title, body, and optional icon.',
{
title: z.string().describe('Notification title'),
body: z.string().describe('Notification body text'),
icon: z.string().optional().describe('Path to icon file (optional)'),
sound: z.boolean().default(true).describe('Play notification sound'),
},
async ({ title, body, icon, sound }) => {
const iconParam = icon
? `$notify.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon('${icon.replace(/'/g, "''")}');`
: `$notify.Icon = [System.Drawing.SystemIcons]::Information;`;
const ps = `
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$notify = New-Object System.Windows.Forms.NotifyIcon
${iconParam}
$notify.Visible = $true
$notify.BalloonTipTitle = '${title.replace(/'/g, "''")}'
$notify.BalloonTipText = '${body.replace(/'/g, "''")}'
$notify.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Info
$notify.ShowBalloonTip(5000)
# Keep alive briefly so notification displays
Start-Sleep -Milliseconds 100
$notify.Dispose()
"Notification sent: ${title.replace(/"/g, '\\"')}"`;
const result = await runPowerShell(ps, { timeout: 10000 });
return {
content: [{ type: 'text', text: result.stdout || result.stderr }],
isError: result.exitCode !== 0,
};
},
);
}
+211
View File
@@ -0,0 +1,211 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Tools: windows_window_list (#14), windows_window_control (#15)
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { runPowerShell } from '../shell.js';
const WIN32_TYPES = `
Add-Type @'
using System;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public class WindowManager {
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
[DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")] public static extern bool IsZoomed(IntPtr hWnd);
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int Left, Top, Right, Bottom; }
public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
public const uint SWP_NOMOVE = 0x0002;
public const uint SWP_NOSIZE = 0x0001;
public const uint WM_CLOSE = 0x0010;
}
'@ -ErrorAction SilentlyContinue
`;
export function registerWindowTools(server: McpServer): void {
server.tool(
'windows_window_list',
'List all visible windows with title, PID, process name, position, size, and state.',
{
filter: z.string().optional().describe('Filter by window title (substring)'),
},
async ({ filter }) => {
const filterClause = filter
? `.Where({ $_.Title -like '*${filter.replace(/'/g, "''")}*' })`
: '';
const ps = `
${WIN32_TYPES}
$windows = [System.Collections.Generic.List[PSObject]]::new()
$zOrder = 0
[WindowManager]::EnumWindows({
param($hWnd, $lParam)
if (-not [WindowManager]::IsWindowVisible($hWnd)) { return $true }
$len = [WindowManager]::GetWindowTextLength($hWnd)
if ($len -eq 0) { return $true }
$sb = New-Object System.Text.StringBuilder($len + 1)
[WindowManager]::GetWindowText($hWnd, $sb, $sb.Capacity) | Out-Null
$title = $sb.ToString()
if (-not $title) { return $true }
$pid = [uint32]0
[WindowManager]::GetWindowThreadProcessId($hWnd, [ref]$pid) | Out-Null
$proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
$rect = New-Object WindowManager+RECT
[WindowManager]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
$state = 'Normal'
if ([WindowManager]::IsIconic($hWnd)) { $state = 'Minimized' }
elseif ([WindowManager]::IsZoomed($hWnd)) { $state = 'Maximized' }
$script:windows.Add([PSCustomObject]@{
ZOrder = $script:zOrder++
Title = $title
PID = $pid
Process = if ($proc) { $proc.ProcessName } else { '?' }
X = $rect.Left
Y = $rect.Top
Width = $rect.Right - $rect.Left
Height = $rect.Bottom - $rect.Top
State = $state
})
return $true
}, [IntPtr]::Zero) | Out-Null
$windows ${filterClause} | ConvertTo-Json -Depth 3 -Compress`;
const result = await runPowerShell(ps, { timeout: 10000 });
if (result.exitCode !== 0) {
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
}
if (!result.stdout) {
return { content: [{ type: 'text', text: 'No visible windows found.' }] };
}
const windows = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
const lines = windows.map((w: { ZOrder: number; Title: string; PID: number; Process: string; X: number; Y: number; Width: number; Height: number; State: string }) => {
const stateIcon = w.State === 'Minimized' ? '[-]' : w.State === 'Maximized' ? '[+]' : '[ ]';
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)}`;
});
const header = `Sta ${'PID'.padStart(6)} ${'Process'.padEnd(20)} ${'Size'.padStart(11)} Pos Title`;
return {
content: [{ type: 'text', text: `${header}\n${'─'.repeat(110)}\n${lines.join('\n')}\n\n${windows.length} windows` }],
};
},
);
server.tool(
'windows_window_control',
'Move, resize, minimize, maximize, restore, close, or focus a window.',
{
title: z.string().optional().describe('Window title (substring match)'),
pid: z.number().optional().describe('Process ID'),
action: z.enum(['minimize', 'maximize', 'restore', 'close', 'focus', 'move', 'resize', 'topmost']).describe('Action'),
x: z.number().optional().describe('X position (for move)'),
y: z.number().optional().describe('Y position (for move)'),
width: z.number().optional().describe('Width (for resize)'),
height: z.number().optional().describe('Height (for resize)'),
topmost: z.boolean().optional().describe('Set always-on-top (for topmost action)'),
},
async ({ title, pid, action, x, y, width, height, topmost }) => {
if (!title && !pid) {
return { content: [{ type: 'text', text: 'Provide either title or pid.' }], isError: true };
}
const findWindow = title
? `
$target = '${title.replace(/'/g, "''")}'
$hWnd = [IntPtr]::Zero
[WindowManager]::EnumWindows({
param($h, $l)
$sb = New-Object System.Text.StringBuilder 256
[WindowManager]::GetWindowText($h, $sb, 256) | Out-Null
if ($sb.ToString() -like "*$target*" -and [WindowManager]::IsWindowVisible($h)) {
$script:hWnd = $h; return $false
}
return $true
}, [IntPtr]::Zero) | Out-Null
if ($hWnd -eq [IntPtr]::Zero) { throw "Window not found: $target" }`
: `
$proc = Get-Process -Id ${pid} -ErrorAction Stop
$hWnd = $proc.MainWindowHandle
if ($hWnd -eq [IntPtr]::Zero) { throw "Process ${pid} has no visible window" }`;
let actionCode: string;
switch (action) {
case 'minimize':
actionCode = `[WindowManager]::ShowWindow($hWnd, 6) | Out-Null; "Minimized"`;
break;
case 'maximize':
actionCode = `[WindowManager]::ShowWindow($hWnd, 3) | Out-Null; "Maximized"`;
break;
case 'restore':
actionCode = `[WindowManager]::ShowWindow($hWnd, 9) | Out-Null; "Restored"`;
break;
case 'close':
actionCode = `[WindowManager]::PostMessage($hWnd, [WindowManager]::WM_CLOSE, [IntPtr]::Zero, [IntPtr]::Zero) | Out-Null; "Close message sent"`;
break;
case 'focus':
actionCode = `[WindowManager]::ShowWindow($hWnd, 9) | Out-Null; [WindowManager]::SetForegroundWindow($hWnd) | Out-Null; "Focused"`;
break;
case 'move':
if (x === undefined || y === undefined) {
return { content: [{ type: 'text', text: 'Move requires x and y.' }], isError: true };
}
actionCode = `
$rect = New-Object WindowManager+RECT
[WindowManager]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
$w = $rect.Right - $rect.Left; $h = $rect.Bottom - $rect.Top
[WindowManager]::MoveWindow($hWnd, ${x}, ${y}, $w, $h, $true) | Out-Null
"Moved to (${x}, ${y})"`;
break;
case 'resize':
if (!width || !height) {
return { content: [{ type: 'text', text: 'Resize requires width and height.' }], isError: true };
}
actionCode = `
$rect = New-Object WindowManager+RECT
[WindowManager]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
[WindowManager]::MoveWindow($hWnd, $rect.Left, $rect.Top, ${width}, ${height}, $true) | Out-Null
"Resized to ${width}x${height}"`;
break;
case 'topmost':
const insertAfter = topmost !== false ? '[WindowManager]::HWND_TOPMOST' : '[WindowManager]::HWND_NOTOPMOST';
actionCode = `[WindowManager]::SetWindowPos($hWnd, ${insertAfter}, 0, 0, 0, 0, [WindowManager]::SWP_NOMOVE -bor [WindowManager]::SWP_NOSIZE) | Out-Null; "Topmost: ${topmost !== false}"`;
break;
}
const ps = `${WIN32_TYPES}\n${findWindow}\n${actionCode}`;
const result = await runPowerShell(ps, { timeout: 10000 });
return {
content: [{ type: 'text', text: result.stdout || result.stderr }],
isError: result.exitCode !== 0,
};
},
);
}