Files
mcp-windows/src/tools/window.ts
T
Jonathan Miller 327b51589a
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
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Universal: Changelog Validation / Validate CHANGELOG.md (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
MCP: Standards Compliance / Secret Scanning (push) Has been cancelled
MCP: Standards Compliance / License Header Validation (push) Has been cancelled
MCP: Build & Validate / build (20) (push) Has been cancelled
MCP: Build & Validate / build (22) (push) Has been cancelled
MCP: Standards Compliance / Repository Structure Validation (push) Has been cancelled
MCP: Standards Compliance / Coding Standards Check (push) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (push) Has been cancelled
MCP: Standards Compliance / Workflow Configuration Check (push) Has been cancelled
MCP: Standards Compliance / Documentation Quality Check (push) Has been cancelled
MCP: Standards Compliance / README Completeness Check (push) Has been cancelled
MCP: Standards Compliance / Git Repository Hygiene (push) Has been cancelled
MCP: Standards Compliance / Line Length Check (push) Has been cancelled
MCP: Standards Compliance / File Naming Standards (push) Has been cancelled
MCP: Standards Compliance / Insecure Code Pattern Detection (push) Has been cancelled
MCP: Standards Compliance / Script Integrity Validation (push) Has been cancelled
MCP: Standards Compliance / Dead Code Detection (push) Has been cancelled
MCP: Standards Compliance / File Size Limits (push) Has been cancelled
MCP: Standards Compliance / Binary File Detection (push) Has been cancelled
MCP: Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
MCP: Build & Release / Build, Validate & Release (push) Has been cancelled
MCP: Standards Compliance / Broken Link Detection (push) Has been cancelled
MCP: Standards Compliance / API Documentation Coverage (push) Has been cancelled
MCP: Standards Compliance / Accessibility Check (push) Has been cancelled
MCP: Standards Compliance / Performance Metrics (push) Has been cancelled
MCP: Standards Compliance / Version Consistency Check (push) Has been cancelled
Universal: CodeQL Analysis / Analyze (actions) (push) Has been cancelled
MCP: Standards Compliance / Code Complexity Analysis (push) Has been cancelled
MCP: Standards Compliance / Code Duplication Detection (push) Has been cancelled
MCP: Standards Compliance / Unused Dependencies Check (push) Has been cancelled
MCP: Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
MCP: Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Universal: CodeQL Analysis / Analyze (javascript) (push) Has been cancelled
Universal: CodeQL Analysis / Security Scan Summary (push) Has been cancelled
MCP: Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
MCP: Standards Compliance / Repository Health Check (push) Has been cancelled
MCP: Standards Compliance / Compliance Summary (push) Has been cancelled
Universal: Sync Version on Merge / Propagate README version (push) Has been cancelled
fix: resolve 6 test failures — timeouts and window_list syntax
- audio.ts: increase Add-Type C# compilation timeout to 60s
- system.ts: increase WMI query timeout to 45s
- service.ts: batch WMI lookups (single query instead of per-service), 45s timeout
- power.ts: increase powercfg timeout to 30s
- window.ts: fix .Where() syntax to pipe-based Where-Object
- shell.ts: unref terminal session child processes so MCP server can exit cleanly

Test results: 36/36 passed (12 skipped — destructive/interactive)

Authored-by: Moko Consulting
2026-05-25 22:27:42 -05:00

212 lines
8.9 KiB
TypeScript

/* 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-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,
};
},
);
}