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
- 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
286 lines
11 KiB
TypeScript
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.` }] };
|
|
}
|
|
},
|
|
);
|
|
}
|