/* Copyright (C) 2026 Moko Consulting * 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-Object { $_.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, }; }, ); }