From a4a7ab57b4864241d32758aef115ff0077f57557 Mon Sep 17 00:00:00 2001 From: triepod-ai Date: Sun, 21 Dec 2025 07:51:52 -0600 Subject: [PATCH] feat(snap-happy): Add Windows/WSL2 support and MCP tool annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Windows/WSL2 Support - Add WSL2 detection via WSL_DISTRO_NAME environment variable - Implement automatic path conversion from WSL (/mnt/c/) to Windows (C:\) - Add window-specific capture on Windows using PrintWindow Win32 API - Implement ListWindows on Windows via PowerShell - Add PowerShell temp script handling with proper cleanup - Add debug logging for troubleshooting ## Screenshot Optimization Options - Add maxWidth/maxHeight parameters for resizing (default: 1920x1080) - Add format option (png/jpeg) - Add quality option for JPEG compression (1-100) - Add returnPath option to return file path instead of base64 ## MCP Tool Annotations - Add annotations to GetLastScreenshot (readOnlyHint: true) - Add annotations to TakeScreenshot (destructiveHint: false, idempotentHint: false) - Add annotations to ListWindows (readOnlyHint: true) - All tools have openWorldHint: false (local system only) ## Documentation Updates - Update tool descriptions to mention Windows support - Update parameter descriptions for new options Reference implementation: https://github.com/triepod-ai/snap-happy-mcp 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/snap-happy/src/index.ts | 32 +- apps/snap-happy/src/screenshot.ts | 499 ++++++++++++++++++++++++++++-- apps/snap-happy/src/tools.ts | 53 +++- 3 files changed, 553 insertions(+), 31 deletions(-) diff --git a/apps/snap-happy/src/index.ts b/apps/snap-happy/src/index.ts index d66f2ac..47df555 100644 --- a/apps/snap-happy/src/index.ts +++ b/apps/snap-happy/src/index.ts @@ -87,20 +87,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "TakeScreenshot": { - const windowId = request.params.arguments?.windowId as number | undefined; - const screenshotPath = takeScreenshot(config.screenshotPath, windowId); + const args = request.params.arguments || {}; + const windowId = args.windowId as number | undefined; + const options = { + maxWidth: args.maxWidth as number | undefined, + maxHeight: args.maxHeight as number | undefined, + quality: args.quality as number | undefined, + format: args.format as 'png' | 'jpeg' | undefined, + returnPath: args.returnPath as boolean | undefined, + }; + + const screenshotPath = takeScreenshot(config.screenshotPath, windowId, options); + + // If returnPath is true, just return the file path + if (options.returnPath) { + return { + content: [ + { + type: "text", + text: `Screenshot saved to: ${screenshotPath}${windowId ? ` (window ID: ${windowId})` : ""}\nOptimization: ${options.maxWidth || 1920}x${options.maxHeight || 1080}, ${options.format || 'png'}${options.format === 'jpeg' ? `, quality: ${options.quality || 80}` : ""}`, + }, + ], + }; + } + + // Return base64 data as before const base64Data = imageToBase64(screenshotPath); + const mimeType = options.format === 'jpeg' ? 'image/jpeg' : 'image/png'; return { content: [ { type: "text", - text: `Screenshot taken: ${screenshotPath}${windowId ? ` (window ID: ${windowId})` : ""}`, + text: `Screenshot taken: ${screenshotPath}${windowId ? ` (window ID: ${windowId})` : ""}\nOptimization: ${options.maxWidth || 1920}x${options.maxHeight || 1080}, ${options.format || 'png'}${options.format === 'jpeg' ? `, quality: ${options.quality || 80}` : ""}`, }, { type: "image", data: base64Data, - mimeType: "image/png", + mimeType: mimeType, }, ], }; diff --git a/apps/snap-happy/src/screenshot.ts b/apps/snap-happy/src/screenshot.ts index 32dc3e5..d2d8740 100644 --- a/apps/snap-happy/src/screenshot.ts +++ b/apps/snap-happy/src/screenshot.ts @@ -4,6 +4,18 @@ import { join, dirname } from "path"; import { platform } from "os"; import { fileURLToPath } from "url"; +/** + * Get current platform with enhanced Windows detection for WSL scenarios + * @returns Platform string compatible with platform detection + */ +function getCurrentPlatform(): string { + // Force Windows detection if we're in WSL or actual Windows + if (process.env.WSL_DISTRO_NAME || process.platform === 'win32') { + return 'win32'; + } + return platform(); +} + /** * Configuration interface for screenshot operations */ @@ -11,6 +23,17 @@ export interface ScreenshotConfig { screenshotPath: string; } +/** + * Options for screenshot optimization + */ +export interface ScreenshotOptions { + maxWidth?: number; + maxHeight?: number; + quality?: number; // 1-100 for JPEG + format?: 'png' | 'jpeg'; + returnPath?: boolean; // If true, return file path instead of base64 +} + /** * Interface for window information */ @@ -49,19 +72,30 @@ export function validateScreenshotPath(path: string): void { * Uses platform-specific commands: screencapture (macOS), gnome-screenshot/scrot (Linux), PowerShell (Windows) * @param screenshotPath - Directory where the screenshot should be saved * @param windowId - Optional window ID to capture specific window (macOS only, uses window geometry) + * @param options - Optional optimization settings for compression, resizing, and format * @returns The full path to the saved screenshot file * @throws Error if screenshot capture fails */ -export function takeScreenshot(screenshotPath: string, windowId?: number): string { +export function takeScreenshot(screenshotPath: string, windowId?: number, options?: ScreenshotOptions): string { + // Set default options + const opts: ScreenshotOptions = { + maxWidth: 1920, + maxHeight: 1080, + quality: 80, + format: 'png', + returnPath: false, + ...options + }; + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").split("T").join("-").split(".")[0]; - const filename = `${timestamp}.png`; + const filename = `${timestamp}.${opts.format}`; const filepath = join(screenshotPath, filename); - const currentPlatform = platform(); + const currentPlatform = getCurrentPlatform(); - // Window-specific screenshots only supported on macOS - if (windowId && currentPlatform !== "darwin") { - throw new Error("Window-specific screenshots are only supported on macOS"); + // Window-specific screenshots supported on macOS and Windows + if (windowId && currentPlatform !== "darwin" && currentPlatform !== "win32") { + throw new Error("Window-specific screenshots are only supported on macOS and Windows"); } try { @@ -104,18 +138,145 @@ export function takeScreenshot(screenshotPath: string, windowId?: number): strin case "win32": // Windows // Use PowerShell to take screenshot - const psScript = ` - Add-Type -AssemblyName System.Windows.Forms; - Add-Type -AssemblyName System.Drawing; - $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; - $bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height; - $graphics = [System.Drawing.Graphics]::FromImage($bitmap); - $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size); - $bitmap.Save("${filepath.replace(/\\/g, "\\\\")}", [System.Drawing.Imaging.ImageFormat]::Png); - $graphics.Dispose(); - $bitmap.Dispose(); - `; - execSync(`powershell -Command "${psScript}"`, { stdio: "pipe" }); + // Convert WSL paths to Windows paths if needed + let windowsFilepath = filepath; + if (filepath.startsWith('/mnt/')) { + // Convert /mnt/c/path to C:\path format + const pathParts = filepath.split('/'); + if (pathParts.length >= 3) { + const drive = pathParts[2].toUpperCase(); + const remainingPath = pathParts.slice(3).join('\\'); + windowsFilepath = `${drive}:\\${remainingPath}`; + } + } + + // Prepare PowerShell script for either window-specific or full screen capture + let psScriptContent: string; + + if (windowId) { + // Window-specific capture for Windows + const windows = listWindows(); + const targetWindow = windows.find((w) => w.id === windowId); + + if (!targetWindow) { + throw new Error(`Window with ID ${windowId} not found`); + } + + // Use the window handle (cgWindowID) to capture specific window + psScriptContent = `Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +Add-Type -ReferencedAssemblies System.Drawing @" +using System; +using System.Runtime.InteropServices; +using System.Drawing; + +public class WindowCapture { + [DllImport("user32.dll")] + public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags); + + [DllImport("user32.dll")] + public static extern bool IsIconic(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [StructLayout(LayoutKind.Sequential)] + public struct RECT { + public int Left; + public int Top; + public int Right; + public int Bottom; + } +} +"@ + +$windowHandle = [IntPtr]${targetWindow.cgWindowID} + +# Restore window if minimized +if ([WindowCapture]::IsIconic($windowHandle)) { + [WindowCapture]::ShowWindow($windowHandle, 9) # SW_RESTORE + Start-Sleep -Milliseconds 100 +} + +# Get window dimensions +$rect = New-Object WindowCapture+RECT +[WindowCapture]::GetWindowRect($windowHandle, [ref]$rect) +$width = $rect.Right - $rect.Left +$height = $rect.Bottom - $rect.Top + +# Create bitmap +$bitmap = New-Object System.Drawing.Bitmap $width, $height +$graphics = [System.Drawing.Graphics]::FromImage($bitmap) +$hdc = $graphics.GetHdc() + +# Capture window +[WindowCapture]::PrintWindow($windowHandle, $hdc, 0x00000002) + +$graphics.ReleaseHdc($hdc) +$graphics.Dispose() + +# Save image +$bitmap.Save("${windowsFilepath}", [System.Drawing.Imaging.ImageFormat]::Png) +$bitmap.Dispose()`; + } else { + // Full screen capture + psScriptContent = `Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing +$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds +$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height +$graphics = [System.Drawing.Graphics]::FromImage($bitmap) +$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) +$bitmap.Save("${windowsFilepath}", [System.Drawing.Imaging.ImageFormat]::Png) +$graphics.Dispose() +$bitmap.Dispose()`; + } + + // Write script to temporary file (use WSL path for writing, Windows path for PowerShell execution) + // Fix: Use robust extension replacement to avoid "undefined" extension issue + const lastDotIndex = filepath.lastIndexOf('.'); + const tempScriptWslPath = lastDotIndex > -1 ? + filepath.substring(0, lastDotIndex) + '.ps1' : + filepath + '.ps1'; + const lastDotIndexWin = windowsFilepath.lastIndexOf('.'); + const tempScriptWinPath = lastDotIndexWin > -1 ? + windowsFilepath.substring(0, lastDotIndexWin) + '.ps1' : + windowsFilepath + '.ps1'; + + writeFileSync(tempScriptWslPath, psScriptContent); + + // Debug logging for troubleshooting + console.error(`[DEBUG] PowerShell script path (WSL): ${tempScriptWslPath}`); + console.error(`[DEBUG] PowerShell script path (Win): ${tempScriptWinPath}`); + + try { + // Use full path to PowerShell when running from WSL + const powershellPath = process.env.WSL_DISTRO_NAME ? + "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" : + "powershell"; + + console.error(`[DEBUG] PowerShell executable: ${powershellPath}`); + execSync(`"${powershellPath}" -ExecutionPolicy Bypass -File "${tempScriptWinPath}"`, { stdio: "pipe" }); + } catch (error) { + // Enhanced error reporting + console.error(`[ERROR] PowerShell execution failed: ${error instanceof Error ? error.message : String(error)}`); + const powershellPath = process.env.WSL_DISTRO_NAME ? + "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" : + "powershell"; + console.error(`[ERROR] Command attempted: "${powershellPath}" -ExecutionPolicy Bypass -File "${tempScriptWinPath}"`); + console.error(`[ERROR] Script exists (WSL path): ${existsSync(tempScriptWslPath)}`); + throw error; + } finally { + // Clean up temporary script file + try { + unlinkSync(tempScriptWslPath); + } catch (e) { + // Ignore cleanup errors + } + } break; default: @@ -127,7 +288,10 @@ export function takeScreenshot(screenshotPath: string, windowId?: number): strin throw new Error("Screenshot file was not created"); } - return filepath; + // Apply optimization if needed + const finalPath = optimizeScreenshot(filepath, opts); + + return finalPath; } catch (error) { throw new Error(`Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`); } @@ -194,16 +358,173 @@ export function getScreenshotConfig(): ScreenshotConfig { return { screenshotPath }; } +/** + * Post-processes a screenshot file to apply optimization settings + * @param originalPath - Path to the original screenshot + * @param options - Optimization options + * @returns Path to the optimized screenshot + */ +function optimizeScreenshot(originalPath: string, options: ScreenshotOptions): string { + const currentPlatform = getCurrentPlatform(); + + // If no optimization needed, return original path + if (!options.maxWidth && !options.maxHeight && options.format === 'png') { + return originalPath; + } + + // Generate optimized file path + const dir = dirname(originalPath); + const ext = options.format || 'png'; + const basename = originalPath.replace(/\.(png|jpeg)$/, ''); + const optimizedPath = `${basename}-opt.${ext}`; + + switch (currentPlatform) { + case "win32": + return optimizeScreenshotWindows(originalPath, optimizedPath, options); + default: + // For macOS/Linux, return original for now (could add ImageMagick support) + return originalPath; + } +} + +/** + * Optimizes screenshot on Windows using PowerShell + */ +function optimizeScreenshotWindows(originalPath: string, optimizedPath: string, options: ScreenshotOptions): string { + try { + // Convert paths to Windows format + let originalWinPath = originalPath; + let optimizedWinPath = optimizedPath; + + if (originalPath.startsWith('/mnt/')) { + const pathParts = originalPath.split('/'); + if (pathParts.length >= 3) { + const drive = pathParts[2].toUpperCase(); + const remainingPath = pathParts.slice(3).join('\\'); + originalWinPath = `${drive}:\\${remainingPath}`; + } + } + + if (optimizedPath.startsWith('/mnt/')) { + const pathParts = optimizedPath.split('/'); + if (pathParts.length >= 3) { + const drive = pathParts[2].toUpperCase(); + const remainingPath = pathParts.slice(3).join('\\'); + optimizedWinPath = `${drive}:\\${remainingPath}`; + } + } + + const powershellPath = process.env.WSL_DISTRO_NAME ? + "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" : + "powershell"; + + const scriptParts = [ + 'Add-Type -AssemblyName System.Drawing', + `$originalImage = [System.Drawing.Image]::FromFile("${originalWinPath}")`, + '$originalWidth = $originalImage.Width', + '$originalHeight = $originalImage.Height', + `$maxWidth = ${options.maxWidth || 1920}`, + `$maxHeight = ${options.maxHeight || 1080}`, + '', + 'if ($originalWidth -gt $maxWidth -or $originalHeight -gt $maxHeight) {', + ' $ratioX = $maxWidth / $originalWidth', + ' $ratioY = $maxHeight / $originalHeight', + ' $ratio = [Math]::Min($ratioX, $ratioY)', + ' $newWidth = [int]($originalWidth * $ratio)', + ' $newHeight = [int]($originalHeight * $ratio)', + '} else {', + ' $newWidth = $originalWidth', + ' $newHeight = $originalHeight', + '}', + '', + '$resizedBitmap = New-Object System.Drawing.Bitmap $newWidth, $newHeight', + '$graphics = [System.Drawing.Graphics]::FromImage($resizedBitmap)', + '$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic', + '$graphics.DrawImage($originalImage, 0, 0, $newWidth, $newHeight)' + ]; + + if (options.format === 'jpeg') { + scriptParts.push( + '$encoder = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq "image/jpeg" }', + '$encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)', + `$encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, ${options.quality || 80}L)`, + `$resizedBitmap.Save("${optimizedWinPath}", $encoder, $encoderParams)` + ); + } else { + scriptParts.push(`$resizedBitmap.Save("${optimizedWinPath}", [System.Drawing.Imaging.ImageFormat]::Png)`); + } + + scriptParts.push( + '$graphics.Dispose()', + '$resizedBitmap.Dispose()', + '$originalImage.Dispose()' + ); + + const psScript = scriptParts.join('\n'); + // Fix: Use robust extension replacement to avoid "undefined" extension issue + const lastDotIndex = originalPath.lastIndexOf('.'); + const tempScriptPath = lastDotIndex > -1 ? + originalPath.substring(0, lastDotIndex) + '-opt.ps1' : + originalPath + '-opt.ps1'; + const lastDotIndexWin = originalWinPath.lastIndexOf('.'); + const tempScriptWinPath = lastDotIndexWin > -1 ? + originalWinPath.substring(0, lastDotIndexWin) + '-opt.ps1' : + originalWinPath + '-opt.ps1'; + + writeFileSync(tempScriptPath, psScript); + + // Debug logging for optimization troubleshooting + console.error(`[DEBUG] Optimization script path (WSL): ${tempScriptPath}`); + console.error(`[DEBUG] Optimization script path (Win): ${tempScriptWinPath}`); + + try { + execSync(`"${powershellPath}" -ExecutionPolicy Bypass -File "${tempScriptWinPath}"`, { stdio: "pipe" }); + + // Clean up original and temp files + unlinkSync(originalPath); + unlinkSync(tempScriptPath); + + return optimizedPath; + } catch (error) { + // Enhanced error reporting for optimization + console.error(`[ERROR] Screenshot optimization failed: ${error instanceof Error ? error.message : String(error)}`); + console.error(`[ERROR] Original path: ${originalPath}, Optimized path: ${optimizedPath}`); + // If optimization fails, clean up and return original + try { unlinkSync(tempScriptPath); } catch (e) { /* ignore */ } + return originalPath; + } + + } catch (error) { + return originalPath; // Fallback to original if optimization fails + } +} + +/** + * Lists all visible windows using platform-specific methods + * Returns window information including position and size + * @returns Array of window information (id, title, app, position, size) + * @throws Error if platform not supported or utility fails + */ +export function listWindows(): WindowInfo[] { + const currentPlatform = getCurrentPlatform(); + + switch (currentPlatform) { + case "darwin": + return listWindowsMacOS(); + case "win32": + return listWindowsWindows(); + default: + throw new Error(`Window listing is not supported on ${currentPlatform}`); + } +} + /** * Lists all visible windows on macOS using native utility * Returns window information including position and size for use with screencapture -R * @returns Array of window information (id, title, app, position, size) * @throws Error if not on macOS or native utility fails */ -export function listWindows(): WindowInfo[] { - if (platform() !== "darwin") { - throw new Error("Window listing is only supported on macOS"); - } +function listWindowsMacOS(): WindowInfo[] { try { // Use the native Swift utility to get window information @@ -244,3 +565,135 @@ export function listWindows(): WindowInfo[] { ); } } + +/** + * Lists all visible windows on Windows using PowerShell + * Returns window information including position and size + * Uses -File approach instead of -Command for better reliability + * @returns Array of window information (id, title, app, position, size) + * @throws Error if PowerShell execution fails + */ +function listWindowsWindows(): WindowInfo[] { + try { + // Use PowerShell to get window information + const powershellPath = process.env.WSL_DISTRO_NAME ? + "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" : + "powershell"; + + const psScript = ` +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing +Add-Type @" +using System; +using System.Runtime.InteropServices; +using System.Text; + +public class Win32 { + [DllImport("user32.dll")] + public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + public static extern bool IsWindowVisible(IntPtr hWnd); + + [StructLayout(LayoutKind.Sequential)] + public struct RECT { + public int Left; + public int Top; + public int Right; + public int Bottom; + } +} +"@ + +$windows = @() +$processes = Get-Process | Where-Object { $_.MainWindowTitle -ne "" -and $_.MainWindowHandle -ne 0 } + +foreach ($process in $processes) { + if ([Win32]::IsWindowVisible($process.MainWindowHandle)) { + $rect = New-Object Win32+RECT + if ([Win32]::GetWindowRect($process.MainWindowHandle, [ref]$rect)) { + $window = @{ + id = $process.Id + cgWindowID = $process.MainWindowHandle.ToInt64() + title = $process.MainWindowTitle + app = $process.ProcessName + position = @{ + x = $rect.Left + y = $rect.Top + } + size = @{ + width = $rect.Right - $rect.Left + height = $rect.Bottom - $rect.Top + } + } + $windows += $window + } + } +} + +ConvertTo-Json $windows -Depth 3`; + + // Fix: Use Windows temp directory when running from WSL + let tempScriptPath: string; + let tempScriptWinPath: string; + + if (process.env.WSL_DISTRO_NAME) { + // Use Windows temp directory when in WSL + tempScriptPath = `/mnt/c/Windows/Temp/list-windows-${Date.now()}.ps1`; + tempScriptWinPath = `C:\\Windows\\Temp\\list-windows-${Date.now()}.ps1`; + } else { + // Regular Windows path + tempScriptPath = join(process.env.TEMP || 'C:\\Windows\\Temp', `list-windows-${Date.now()}.ps1`); + tempScriptWinPath = tempScriptPath; + } + + let result: string; + try { + writeFileSync(tempScriptPath, psScript); + + // Debug logging for window listing + console.error(`[DEBUG] ListWindows script path (WSL): ${tempScriptPath}`); + console.error(`[DEBUG] ListWindows script path (Win): ${tempScriptWinPath}`); + + result = execSync(`"${powershellPath}" -ExecutionPolicy Bypass -File "${tempScriptWinPath}"`, { + encoding: "utf8", + stdio: "pipe", + }); + } catch (error) { + // Enhanced error reporting for window listing + console.error(`[ERROR] Window listing failed: ${error instanceof Error ? error.message : String(error)}`); + console.error(`[ERROR] PowerShell path: ${powershellPath}`); + console.error(`[ERROR] Script path: ${tempScriptWinPath}`); + throw error; + } finally { + // Clean up temporary script file + try { + unlinkSync(tempScriptPath); + } catch (e) { + // Ignore cleanup errors + } + } + + if (!result.trim()) { + throw new Error("No windows found"); + } + + const windows: WindowInfo[] = JSON.parse(result.trim()); + + if (windows.length === 0) { + throw new Error("No visible windows found"); + } + + return windows; + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error("Failed to parse window information from PowerShell"); + } + throw new Error( + `Failed to list windows: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} \ No newline at end of file diff --git a/apps/snap-happy/src/tools.ts b/apps/snap-happy/src/tools.ts index 8ec53ae..5e0a953 100644 --- a/apps/snap-happy/src/tools.ts +++ b/apps/snap-happy/src/tools.ts @@ -11,6 +11,11 @@ export const getLastScreenshotTool: Tool = { properties: {}, required: [], }, + annotations: { + title: "Get Last Screenshot", + readOnlyHint: true, + openWorldHint: false, + }, }; /** @@ -19,32 +24,72 @@ export const getLastScreenshotTool: Tool = { export const takeScreenshotTool: Tool = { name: "TakeScreenshot", description: - "Takes a new screenshot, stores it, and returns as base64 encoded PNG data. Optionally capture a specific window by ID (macOS only). If asked to capture a specific app, ALWAYS use ListWindows tool first, to get the windowId. Without windowId parameter, takes a full screen screenshot.", + "Takes a new screenshot with optimization options. Can resize, compress, change format, or return file path only. Optionally capture a specific window by ID (macOS and Windows). If asked to capture a specific app, ALWAYS use ListWindows tool first, to get the windowId. Without windowId parameter, takes a full screen screenshot.", inputSchema: { type: "object", properties: { windowId: { type: "number", description: - "Optional window ID to capture specific window (macOS only). If asked to capture a specific app, ALWAYS use ListWindows tool first, to get the windowId.", + "Optional window ID to capture specific window (macOS and Windows). If asked to capture a specific app, ALWAYS use ListWindows tool first, to get the windowId.", + }, + maxWidth: { + type: "number", + description: "Maximum width for the screenshot (default: 1920). Images larger than this will be resized proportionally.", + default: 1920, + }, + maxHeight: { + type: "number", + description: "Maximum height for the screenshot (default: 1080). Images larger than this will be resized proportionally.", + default: 1080, + }, + quality: { + type: "number", + description: "JPEG quality (1-100, default: 80). Only applies when format is 'jpeg'.", + minimum: 1, + maximum: 100, + default: 80, + }, + format: { + type: "string", + enum: ["png", "jpeg"], + description: "Image format (default: 'png'). JPEG provides smaller file sizes but with compression artifacts.", + default: "png", + }, + returnPath: { + type: "boolean", + description: "If true, returns only the file path instead of base64 image data. Useful for large screenshots to avoid response size limits.", + default: false, }, }, required: [], }, + annotations: { + title: "Take Screenshot", + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, }; /** - * MCP tool definition for listing windows (macOS only) + * MCP tool definition for listing windows (macOS and Windows) */ export const listWindowsTool: Tool = { name: "ListWindows", description: - "Lists all visible windows with their IDs, titles, application names, positions, and sizes (macOS only). Window IDs can be used with TakeScreenshot to capture specific windows. Use this tool FIRST when asked to capture a specific application or window.", + "Lists all visible windows with their IDs, titles, application names, positions, and sizes (macOS and Windows). Window IDs can be used with TakeScreenshot to capture specific windows on both macOS and Windows. Use this tool FIRST when asked to capture a specific application or window.", inputSchema: { type: "object", properties: {}, required: [], }, + annotations: { + title: "List Windows", + readOnlyHint: true, + openWorldHint: false, + }, }; /**