import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { spawn } from "node:child_process";
import clipboardy from "clipboardy";
import { runPowerShell } from "../utils/powershell.js";
export function registerDesktopTools(server: McpServer) {
// desktop_mouse_click
server.tool(
"desktop_mouse_click",
"Click at screen coordinates using the mouse",
{
x: z.number().describe("X coordinate on screen"),
y: z.number().describe("Y coordinate on screen"),
button: z.enum(["left", "right", "middle"]).optional().describe("Mouse button (default: left)"),
clickCount: z.number().optional().describe("Number of clicks (default: 1)"),
},
async ({ x, y, button, clickCount }) => {
try {
const btn = button ?? "left";
const count = clickCount ?? 1;
// Map button to mouse_event flags
let downFlag: string, upFlag: string;
switch (btn) {
case "right":
downFlag = "0x0008"; upFlag = "0x0010"; break;
case "middle":
downFlag = "0x0020"; upFlag = "0x0040"; break;
default:
downFlag = "0x0002"; upFlag = "0x0004"; break;
}
const psScript = `
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class MouseOps {
[DllImport("user32.dll")]
public static extern bool SetCursorPos(int X, int Y);
[DllImport("user32.dll")]
public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo);
}
"@
[MouseOps]::SetCursorPos(${x}, ${y})
for ($i = 0; $i -lt ${count}; $i++) {
[MouseOps]::mouse_event(${downFlag}, 0, 0, 0, [IntPtr]::Zero)
[MouseOps]::mouse_event(${upFlag}, 0, 0, 0, [IntPtr]::Zero)
if ($i -lt ${count - 1}) { Start-Sleep -Milliseconds 50 }
}
`;
await runPowerShell(psScript);
return { content: [{ type: "text", text: `Clicked ${btn} at (${x}, ${y})${count > 1 ? ` x${count}` : ""}` }] };
} catch (err: any) {
return { content: [{ type: "text", text: `Mouse click failed: ${err.message}` }], isError: true };
}
}
);
// desktop_mouse_move
server.tool(
"desktop_mouse_move",
"Move the mouse cursor to screen coordinates",
{
x: z.number().describe("X coordinate"),
y: z.number().describe("Y coordinate"),
smooth: z.boolean().optional().describe("Smooth movement animation (default: false)"),
},
async ({ x, y, smooth }) => {
try {
if (smooth) {
const psScript = `
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class MouseMove {
[DllImport("user32.dll")]
public static extern bool SetCursorPos(int X, int Y);
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT lpPoint);
}
public struct POINT { public int X, Y; }
"@
$p = New-Object POINT
[MouseMove]::GetCursorPos([ref]$p) | Out-Null
$steps = 20
for ($i = 1; $i -le $steps; $i++) {
$cx = [int]($p.X + ($i / $steps) * (${x} - $p.X))
$cy = [int]($p.Y + ($i / $steps) * (${y} - $p.Y))
[MouseMove]::SetCursorPos($cx, $cy) | Out-Null
Start-Sleep -Milliseconds 10
}
`;
await runPowerShell(psScript);
} else {
await runPowerShell(`
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class M { [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); }
"@
[M]::SetCursorPos(${x}, ${y}) | Out-Null
`);
}
return { content: [{ type: "text", text: `Moved cursor to (${x}, ${y})${smooth ? " (smooth)" : ""}` }] };
} catch (err: any) {
return { content: [{ type: "text", text: `Mouse move failed: ${err.message}` }], isError: true };
}
}
);
// desktop_keyboard_type
server.tool(
"desktop_keyboard_type",
"Type text using the keyboard (simulates key presses)",
{
text: z.string().describe("Text to type"),
delayMs: z.number().optional().describe("Delay between keystrokes in ms (default: 0)"),
},
async ({ text, delayMs }) => {
try {
// Escape special characters for PowerShell SendKeys
const escaped = text
.replace(/[+^%~(){}[\]]/g, "{$&}");
const delay = delayMs ?? 0;
const psScript = `
Add-Type -AssemblyName System.Windows.Forms
$text = @'
${escaped}
'@
if (${delay} -gt 0) {
foreach ($char in $text.ToCharArray()) {
[System.Windows.Forms.SendKeys]::SendWait([string]$char)
Start-Sleep -Milliseconds ${delay}
}
} else {
[System.Windows.Forms.SendKeys]::SendWait($text)
}
`;
await runPowerShell(psScript);
return { content: [{ type: "text", text: `Typed: "${text.length > 100 ? text.slice(0, 100) + "..." : text}"` }] };
} catch (err: any) {
return { content: [{ type: "text", text: `Keyboard type failed: ${err.message}` }], isError: true };
}
}
);
// desktop_keyboard_hotkey
server.tool(
"desktop_keyboard_hotkey",
"Press a keyboard shortcut (e.g. ctrl+c, alt+tab)",
{
keys: z.array(z.string()).describe('Key combination, e.g. ["ctrl", "c"] or ["alt", "tab"]'),
},
async ({ keys }) => {
try {
// Map common key names to SendKeys format
const keyMap: Record<string, string> = {
ctrl: "^", control: "^",
alt: "%",
shift: "+",
enter: "{ENTER}", return: "{ENTER}",
tab: "{TAB}",
escape: "{ESC}", esc: "{ESC}",
backspace: "{BACKSPACE}", bs: "{BACKSPACE}",
delete: "{DELETE}", del: "{DELETE}",
up: "{UP}", down: "{DOWN}", left: "{LEFT}", right: "{RIGHT}",
home: "{HOME}", end: "{END}",
pageup: "{PGUP}", pagedown: "{PGDN}",
f1: "{F1}", f2: "{F2}", f3: "{F3}", f4: "{F4}",
f5: "{F5}", f6: "{F6}", f7: "{F7}", f8: "{F8}",
f9: "{F9}", f10: "{F10}", f11: "{F11}", f12: "{F12}",
space: " ", win: "^{ESC}", // Windows key approximation
};
// Build SendKeys string: modifiers wrap the last key
const modifiers: string[] = [];
const regularKeys: string[] = [];
for (const k of keys) {
const lower = k.toLowerCase();
const mapped = keyMap[lower];
if (mapped && (mapped === "^" || mapped === "%" || mapped === "+")) {
modifiers.push(mapped);
} else if (mapped) {
regularKeys.push(mapped);
} else {
// Single character key
regularKeys.push(k.length === 1 ? k : `{${k.toUpperCase()}}`);
}
}
const combo = modifiers.join("") + "(" + regularKeys.join("") + ")";
const psScript = `
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait('${combo.replace(/'/g, "''")}')
`;
await runPowerShell(psScript);
return { content: [{ type: "text", text: `Pressed: ${keys.join("+")}` }] };
} catch (err: any) {
return { content: [{ type: "text", text: `Hotkey failed: ${err.message}` }], isError: true };
}
}
);
// desktop_window_list
server.tool(
"desktop_window_list",
"List all visible windows with their titles, process names, and positions",
{},
async () => {
try {
const psScript = `
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class WinInfo {
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
}
public struct RECT { public int Left, Top, Right, Bottom; }
"@
Get-Process | Where-Object { $_.MainWindowTitle -ne '' -and $_.MainWindowHandle -ne 0 } | ForEach-Object {
$rect = New-Object RECT
[WinInfo]::GetWindowRect($_.MainWindowHandle, [ref]$rect) | Out-Null
[PSCustomObject]@{
Title = $_.MainWindowTitle
Process = $_.ProcessName
PID = $_.Id
X = $rect.Left
Y = $rect.Top
Width = $rect.Right - $rect.Left
Height = $rect.Bottom - $rect.Top
}
} | ConvertTo-Json -Compress
`;
const result = await runPowerShell(psScript);
let windows;
try {
windows = JSON.parse(result);
// Ensure it's always an array
if (!Array.isArray(windows)) windows = [windows];
} catch {
windows = [];
}
const formatted = windows.map((w: any) =>
`${w.Title} | ${w.Process} (PID ${w.PID}) | pos: ${w.X},${w.Y} | size: ${w.Width}x${w.Height}`
).join("\n");
return { content: [{ type: "text", text: formatted || "No visible windows found." }] };
} catch (err: any) {
return { content: [{ type: "text", text: `Window list failed: ${err.message}` }], isError: true };
}
}
);
// desktop_window_focus
server.tool(
"desktop_window_focus",
"Focus a window by title (partial match)",
{
title: z.string().describe("Window title to match (partial)"),
},
async ({ title }) => {
try {
const psScript = `
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class WinFocus {
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
}
"@
$proc = Get-Process | Where-Object { $_.MainWindowTitle -like "*${title}*" -and $_.MainWindowHandle -ne 0 } | Select-Object -First 1
if (-not $proc) { throw "No window found matching '${title}'" }
[WinFocus]::ShowWindow($proc.MainWindowHandle, 9) | Out-Null # SW_RESTORE
[WinFocus]::SetForegroundWindow($proc.MainWindowHandle) | Out-Null
$proc.MainWindowTitle
`;
const result = await runPowerShell(psScript);
return { content: [{ type: "text", text: `Focused window: ${result}` }] };
} catch (err: any) {
return { content: [{ type: "text", text: `Window focus failed: ${err.message}` }], isError: true };
}
}
);
// desktop_window_resize
server.tool(
"desktop_window_resize",
"Resize and/or move a window by title",
{
title: z.string().describe("Window title to match (partial)"),
x: z.number().optional().describe("New X position"),
y: z.number().optional().describe("New Y position"),
width: z.number().optional().describe("New width"),
height: z.number().optional().describe("New height"),
},
async ({ title, x, y, width, height }) => {
try {
const nxExpr = x !== undefined ? `$nx = ${x}` : `$nx = $rect.Left`;
const nyExpr = y !== undefined ? `$ny = ${y}` : `$ny = $rect.Top`;
const nwExpr = width !== undefined ? `$nw = ${width}` : `$nw = $rect.Right - $rect.Left`;
const nhExpr = height !== undefined ? `$nh = ${height}` : `$nh = $rect.Bottom - $rect.Top`;
const psScript = `
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class WinResize {
[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 GetWindowRect(IntPtr hWnd, out RECT lpRect);
}
public struct RECT { public int Left, Top, Right, Bottom; }
"@
$proc = Get-Process | Where-Object { $_.MainWindowTitle -like "*${title}*" -and $_.MainWindowHandle -ne 0 } | Select-Object -First 1
if (-not $proc) { throw "No window found matching '${title}'" }
$hwnd = $proc.MainWindowHandle
$rect = New-Object RECT
[WinResize]::GetWindowRect($hwnd, [ref]$rect) | Out-Null
${nxExpr}
${nyExpr}
${nwExpr}
${nhExpr}
[WinResize]::MoveWindow($hwnd, $nx, $ny, $nw, $nh, $true) | Out-Null
"Resized '$($proc.MainWindowTitle)' to pos ($nx,$ny) size ($nw x $nh)"
`;
const result = await runPowerShell(psScript);
return { content: [{ type: "text", text: result }] };
} catch (err: any) {
return { content: [{ type: "text", text: `Window resize failed: ${err.message}` }], isError: true };
}
}
);
// desktop_app_launch
server.tool(
"desktop_app_launch",
"Launch an application by path, name, or URI",
{
target: z.string().describe("App path, executable name, or URI (e.g. 'notepad', 'https://...', 'ms-settings:')"),
args: z.array(z.string()).optional().describe("Command line arguments"),
waitMs: z.number().optional().describe("Time to wait after launch in ms (default: 1000)"),
},
async ({ target, args, waitMs }) => {
try {
const child = spawn(target, args ?? [], {
detached: true,
stdio: "ignore",
shell: true,
});
child.unref();
const wait = waitMs ?? 1000;
await new Promise(resolve => setTimeout(resolve, wait));
return { content: [{ type: "text", text: `Launched: ${target}${args?.length ? " " + args.join(" ") : ""}` }] };
} catch (err: any) {
return { content: [{ type: "text", text: `App launch failed: ${err.message}` }], isError: true };
}
}
);
// desktop_clipboard_read
server.tool(
"desktop_clipboard_read",
"Read the current clipboard text content",
{},
async () => {
try {
const text = await clipboardy.read();
return { content: [{ type: "text", text: text || "(clipboard is empty)" }] };
} catch (err: any) {
return { content: [{ type: "text", text: `Clipboard read failed: ${err.message}` }], isError: true };
}
}
);
// desktop_clipboard_write
server.tool(
"desktop_clipboard_write",
"Write text to the clipboard",
{
text: z.string().describe("Text to write to clipboard"),
},
async ({ text }) => {
try {
await clipboardy.write(text);
return { content: [{ type: "text", text: `Wrote ${text.length} characters to clipboard.` }] };
} catch (err: any) {
return { content: [{ type: "text", text: `Clipboard write failed: ${err.message}` }], isError: true };
}
}
);
}