/* Copyright (C) 2026 Moko Consulting * 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.` }] }; } }, ); }