Files
mcp-windows/src/tools/display.ts
T
Jonathan Miller 2b29940d8e
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
feat: implement v1.2 desktop automation tools (8 new tools)
- 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
2026-05-25 21:28:46 -05:00

286 lines
11 KiB
TypeScript

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