UNPKG

mcp-desktop-pro

Version:

Advanced MCP server for intelligent computer use automation and AI agent operator control. Supports mouse and keyboard input, window-relative vision and coordinates, Retina display, and screenshot-based debugging. Designed for AI-driven workflows, app and

1,170 lines (997 loc) 73.7 kB
#!/usr/bin/env node // Import required packages const screenshot = require('screenshot-desktop'); const robot = require('robotjs'); const { McpServer, ResourceTemplate } = require("@modelcontextprotocol/sdk/server/mcp.js"); const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js"); const { z } = require("zod"); const fs = require('fs'); const packageJson = require('./package.json'); // Create server instance const server = new McpServer( { name: "mcp-desktop-automation", version: packageJson.version, }, { capabilities: { resources: {}, tools: {}, logging: {}, }, }, ); const screenshots = {}; // Debug logging utility const path = require('path'); const os = require('os'); // Platform detection const platform = os.platform(); const isWindows = platform === 'win32'; const isMacOS = platform === 'darwin'; const isLinux = platform === 'linux'; // Parse command line arguments const args = process.argv.slice(2); const isGlobalDebugEnabled = args.includes('--debug'); const showHelp = args.includes('--help') || args.includes('-h'); // Show help if requested if (showHelp) { console.log(` MCP Desktop Pro - Advanced Desktop Automation Server USAGE: node server.js [OPTIONS] OPTIONS: --debug Enable comprehensive debug logging - Creates debug.log file with detailed operation logs - Saves debug screenshots for mouse operations - Logs coordinate transformations and error details --help, -h Show this help message EXAMPLES: node server.js # Start server in normal mode node server.js --debug # Start server with debug logging enabled DESCRIPTION: MCP Desktop Pro provides comprehensive desktop automation capabilities through the Model Context Protocol (MCP). It offers: • Mouse control with intelligent coordinate scaling • Keyboard input and key combinations • Screen and window capture with AI optimization • Window management and focus control • Multi-action sequences with error handling The server runs on stdio and communicates via the MCP protocol. Debug mode is recommended for troubleshooting coordinate issues. MORE INFO: See CLAUDE.md for detailed architecture and usage documentation. `); process.exit(0); } const debugLog = (message, debugEnabled = false) => { // Enable debug if either global flag is set OR local debugEnabled is true if (!debugEnabled && !isGlobalDebugEnabled) return; const debugLogPath = path.join(__dirname, 'debug.log'); try { // Ensure the debug log exists if (!fs.existsSync(debugLogPath)) { fs.writeFileSync(debugLogPath, ''); } fs.appendFileSync(debugLogPath, message); } catch (error) { // Silently ignore debug logging errors to avoid breaking MCP protocol } }; // Cross-platform window management helpers const windowHelpers = { async focusWindow(targetWindow) { const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); if (isMacOS) { // macOS: Use AppleScript with simple single-line commands const appName = targetWindow.owner.name; try { await execAsync(`osascript -e 'tell application "${appName}" to activate'`); return { success: true }; } catch (scriptError) { // Fallback: try with bundle ID try { await execAsync(`osascript -e 'tell application id "${targetWindow.owner.bundleId}" to activate'`); return { success: true }; } catch (bundleError) { return { success: false, error: `Failed to focus window: ${scriptError.message}` }; } } } else if (isWindows) { // Windows: Use PowerShell to focus window try { const processId = targetWindow.owner.processId; const powershellScript = ` Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class Win32 { [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); }' $process = Get-Process -Id ${processId} -ErrorAction SilentlyContinue if ($process -and $process.MainWindowHandle -ne [IntPtr]::Zero) { [Win32]::ShowWindow($process.MainWindowHandle, 9) [Win32]::SetForegroundWindow($process.MainWindowHandle) } `; await execAsync(`powershell -Command "${powershellScript}"`); return { success: true }; } catch (error) { return { success: false, error: `Failed to focus window on Windows: ${error.message}` }; } } else if (isLinux) { // Linux: Use wmctrl or xdotool try { const windowTitle = targetWindow.title; // Try wmctrl first await execAsync(`wmctrl -a "${windowTitle}"`); return { success: true }; } catch (wmctrlError) { try { // Fallback to xdotool await execAsync(`xdotool search --name "${targetWindow.title}" windowactivate`); return { success: true }; } catch (xdotoolError) { return { success: false, error: `Failed to focus window on Linux: wmctrl: ${wmctrlError.message}, xdotool: ${xdotoolError.message}` }; } } } else { return { success: false, error: `Window focusing not implemented for platform: ${platform}` }; } }, async moveWindow(targetWindow, newX, newY, newWidth, newHeight) { const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); if (isMacOS) { // macOS: Use AppleScript (existing implementation) return await this.moveWindowMacOS(targetWindow, newX, newY, newWidth, newHeight); } else if (isWindows) { // Windows: Use PowerShell try { const processId = targetWindow.owner.processId; const powershellScript = ` Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class Win32 { [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); }' $process = Get-Process -Id ${processId} -ErrorAction SilentlyContinue if ($process -and $process.MainWindowHandle -ne [IntPtr]::Zero) { [Win32]::SetWindowPos($process.MainWindowHandle, [IntPtr]::Zero, ${newX}, ${newY}, ${newWidth}, ${newHeight}, 0x0040) } `; await execAsync(`powershell -Command "${powershellScript}"`); return { success: true, message: `Window moved on Windows to position (${newX}, ${newY}) with size ${newWidth}x${newHeight}`, newBounds: { x: newX, y: newY, width: newWidth, height: newHeight } }; } catch (error) { return { success: false, error: `Failed to move window on Windows: ${error.message}` }; } } else if (isLinux) { // Linux: Use wmctrl try { const windowTitle = targetWindow.title; await execAsync(`wmctrl -r "${windowTitle}" -e 0,${newX},${newY},${newWidth},${newHeight}`); return { success: true, message: `Window moved on Linux to position (${newX}, ${newY}) with size ${newWidth}x${newHeight}`, newBounds: { x: newX, y: newY, width: newWidth, height: newHeight } }; } catch (error) { return { success: false, error: `Failed to move window on Linux: ${error.message}. Make sure wmctrl is installed: sudo apt-get install wmctrl` }; } } else { return { success: false, error: `Window moving not implemented for platform: ${platform}` }; } }, async moveWindowMacOS(targetWindow, newX, newY, newWidth, newHeight) { // Existing macOS AppleScript implementation const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); const appName = targetWindow.owner.name; // Special handling for sandboxed apps like Calculator if (appName === 'Calculator' || targetWindow.owner.bundleId === 'com.apple.calculator') { return await this.moveWindowUsingRobotJS(targetWindow, newX, newY, newWidth, newHeight); } // Method 1: Try position and size separately (works better with Calculator) try { await execAsync(`osascript -e 'tell application "${appName}" to activate'`); await new Promise(resolve => setTimeout(resolve, 200)); // Set position and size separately instead of bounds await execAsync(`osascript -e 'tell application "${appName}" to set position of first window to {${newX}, ${newY}}'`); await execAsync(`osascript -e 'tell application "${appName}" to set size of first window to {${newWidth}, ${newHeight}}'`); return { success: true, message: `Window moved to primary screen at position (${newX}, ${newY}) with size ${newWidth}x${newHeight}`, newBounds: { x: newX, y: newY, width: newWidth, height: newHeight } }; } catch (simpleError) { // Method 2: Try with bundle ID using position and size separately try { await execAsync(`osascript -e 'tell application id "${targetWindow.owner.bundleId}" to activate'`); await new Promise(resolve => setTimeout(resolve, 200)); await execAsync(`osascript -e 'tell application id "${targetWindow.owner.bundleId}" to set position of first window to {${newX}, ${newY}}'`); await execAsync(`osascript -e 'tell application id "${targetWindow.owner.bundleId}" to set size of first window to {${newWidth}, ${newHeight}}'`); return { success: true, message: `Window moved to primary screen at position (${newX}, ${newY}) with size ${newWidth}x${newHeight}`, newBounds: { x: newX, y: newY, width: newWidth, height: newHeight } }; } catch (bundleError) { // Method 3: Try System Events with separate commands try { // First activate the application await execAsync(`osascript -e 'tell application "${appName}" to activate'`); await new Promise(resolve => setTimeout(resolve, 200)); // Then set position and size separately await execAsync(`osascript -e 'tell application "System Events" to tell application process "${appName}" to set position of first window to {${newX}, ${newY}}'`); await execAsync(`osascript -e 'tell application "System Events" to tell application process "${appName}" to set size of first window to {${newWidth}, ${newHeight}}'`); return { success: true, message: `Window moved to primary screen at position (${newX}, ${newY}) with size ${newWidth}x${newHeight} (using System Events)`, newBounds: { x: newX, y: newY, width: newWidth, height: newHeight } }; } catch (systemError) { // Method 4: Try window by index with System Events try { await execAsync(`osascript -e 'tell application "${appName}" to activate'`); await new Promise(resolve => setTimeout(resolve, 200)); await execAsync(`osascript -e 'tell application "System Events" to tell application process "${appName}" to set position of window 1 to {${newX}, ${newY}}'`); await execAsync(`osascript -e 'tell application "System Events" to tell application process "${appName}" to set size of window 1 to {${newWidth}, ${newHeight}}'`); return { success: true, message: `Window moved to primary screen at position (${newX}, ${newY}) with size ${newWidth}x${newHeight} (using System Events window 1)`, newBounds: { x: newX, y: newY, width: newWidth, height: newHeight } }; } catch (indexError) { return { success: false, error: `Failed to move window with all methods. App: "${appName}", Bundle: "${targetWindow.owner.bundleId}". Errors: Simple: ${simpleError.message}, Bundle: ${bundleError.message}, System Events: ${systemError.message}, Index: ${indexError.message}` }; } } } } }, async moveWindowUsingRobotJS(targetWindow, newX, newY, newWidth, newHeight) { try { // Focus the window first by clicking on it const centerX = targetWindow.bounds.x + targetWindow.bounds.width / 2; const centerY = targetWindow.bounds.y + targetWindow.bounds.height / 2; // Click to focus the window robot.moveMouse(centerX, centerY); robot.mouseClick(); await new Promise(resolve => setTimeout(resolve, 100)); // Use keyboard shortcut to enter window move mode (Command+Option+F5 on macOS enables switch control) // Actually, let's use a simpler approach - drag the window // Find the title bar area (top of window) const titleBarY = targetWindow.bounds.y + 10; // Approximate title bar height const titleBarX = targetWindow.bounds.x + targetWindow.bounds.width / 2; // Drag from current position to new position robot.moveMouse(titleBarX, titleBarY); robot.mouseToggle('down'); await new Promise(resolve => setTimeout(resolve, 50)); // Drag to new position robot.moveMouse(newX + newWidth / 2, newY + 10); await new Promise(resolve => setTimeout(resolve, 50)); robot.mouseToggle('up'); // Wait for window to settle await new Promise(resolve => setTimeout(resolve, 200)); return { success: true, message: `Window moved using robotJS drag to position (${newX}, ${newY})`, newBounds: { x: newX, y: newY, width: newWidth, height: newHeight } }; } catch (error) { return { success: false, error: `Failed to move window using robotJS: ${error.message}` }; } } }; // Implementation of capabilities const capabilityImplementations = { screen_capture: async (params = {}) => { try { // Take a screenshot (compress to JPEG to reduce size) let img = await screenshot({ format: 'jpg' }); const sharp = require('sharp'); // Get initial metadata const metadata = await sharp(img).metadata(); const screenSize = robot.getScreenSize(); // Calculate scaling factor for Retina detection const scaleX = metadata.width / screenSize.width; const scaleY = metadata.height / screenSize.height; const isHighDPI = scaleX > 1.5 || scaleY > 1.5; let sharpInstance = sharp(img); // If coordinates are provided, crop the image first if (params.x1 !== undefined && params.y1 !== undefined && params.x2 !== undefined && params.y2 !== undefined) { let scaledX1, scaledY1, scaledX2, scaledY2; if (isHighDPI) { // Apply scaling for high-DPI displays (Retina) scaledX1 = Math.round(params.x1 * scaleX); scaledY1 = Math.round(params.y1 * scaleY); scaledX2 = Math.round(params.x2 * scaleX); scaledY2 = Math.round(params.y2 * scaleY); } else { // Use coordinates as-is for standard displays scaledX1 = params.x1; scaledY1 = params.y1; scaledX2 = params.x2; scaledY2 = params.y2; } const width = scaledX2 - scaledX1; const height = scaledY2 - scaledY1; if (width <= 0 || height <= 0) { throw new Error('Invalid coordinates: x2 must be greater than x1 and y2 must be greater than y1'); } sharpInstance = sharpInstance.extract({ left: scaledX1, top: scaledY1, width: width, height: height }); } // Always apply AI optimization for faster processing // Get current dimensions (after potential cropping) const currentMeta = await sharpInstance.metadata(); const currentWidth = currentMeta.width; const currentHeight = currentMeta.height; // Scale to 50% of original size, but cap at 1280x720 for much smaller responses const targetWidth = Math.min(Math.round(currentWidth * 0.5), 1280); const targetHeight = Math.min(Math.round(currentHeight * 0.5), 720); // Only resize if the target is smaller than current if (targetWidth < currentWidth || targetHeight < currentHeight) { sharpInstance = sharpInstance.resize(targetWidth, targetHeight, { fit: 'inside', withoutEnlargement: true }); } // Apply very aggressive WebP compression for AI processing img = await sharpInstance.webp({ quality: 15, effort: 0 }).toBuffer(); // Check final size and abort if too large (stay well under 1MB limit) const sizeKB = img.length / 1024; if (sizeKB > 300) { return { success: false, error: `Screenshot too large: ${sizeKB.toFixed(1)}KB (max 300KB). Try capturing a smaller area or window.` }; } const imgInBase64 = img.toString('base64'); const timestamp = Math.floor(Date.now() / 1000); const screenshotKey = `screenshot-${timestamp}`; screenshots[screenshotKey] = imgInBase64; await server.server.sendResourceListChanged(); // Calculate actual AI image dimensions const finalMeta = await sharp(img).metadata(); const aiImageWidth = finalMeta.width; const aiImageHeight = finalMeta.height; // Store screen capture metadata for coordinate transformations // We will assume the AI sees the image at the size we report. const aiActualWidth = aiImageWidth; const aiActualHeight = aiImageHeight; const screenCaptureMetadata = { originalSize: { width: metadata.width, height: metadata.height }, logicalScreenSize: { width: screenSize.width, height: screenSize.height }, reportedAiSize: { width: aiImageWidth, height: aiImageHeight }, aiImageSize: { width: aiActualWidth, height: aiActualHeight }, // What AI actually sees timestamp: Date.now() }; // Store in global for mouse_move to access if (!global.screenCaptureMetadata) { global.screenCaptureMetadata = {}; } global.lastScreenCapture = screenCaptureMetadata; return { content: [ { type: "text", text: `Screenshot ${screenshotKey} taken. Screen size: ${screenSize.width}x${screenSize.height}, AI sees: ${aiImageWidth}x${aiImageHeight}`, }, { type: "image", mimeType: "image/webp", data: imgInBase64, }, ], isError: false }; } catch (error) { // Error captured silently to avoid interfering with MCP protocol return { success: false, error: error.message }; } }, mouse_move: async (params) => { try { const { x, y, debug = false, windowId } = params; // Add a timestamped log to mark the beginning of the function call const timestamp = new Date().toISOString(); debugLog(`\n--- MOUSE_MOVE START ${timestamp} ---\n`, debug); debugLog(`Params: ${JSON.stringify(params)}\n`, debug); // Store original coordinates for debug visualization const originalX = x; const originalY = y; let moveX = x; let moveY = y; // Always focus window first if windowId is provided const focusResult = await capabilityImplementations.focus_window({ windowId }); if (!focusResult.success) { return focusResult; } // Wait for window to focus await new Promise(resolve => setTimeout(resolve, 300)); // We only support window-relative coordinates now, as it's the only reliable method. if (!windowId) { return { success: false, error: 'windowId is a required parameter for mouse_move.' }; } // Get or create metadata for this window let metadata; if (!global.windowCaptureMetadata || !global.windowCaptureMetadata[windowId]) { debugLog(`No window capture metadata found for window ${windowId}, creating basic metadata for coordinate transformation\n`, debug); // Get window information to create basic metadata const activeWin = require('active-win'); const windows = await activeWin.getOpenWindows(); const targetWindow = windows.find(w => w && w.id === windowId); if (!targetWindow) { return { success: false, error: `Window not found with ID: ${windowId}` }; } // Create basic metadata assuming no AI image scaling (1:1 coordinate mapping) metadata = { windowId: windowId, originalLogicalSize: { width: targetWindow.bounds.width, height: targetWindow.bounds.height }, aiImageSize: { width: targetWindow.bounds.width, height: targetWindow.bounds.height }, timestamp: Date.now() }; // Store it for future use if (!global.windowCaptureMetadata) { global.windowCaptureMetadata = {}; } global.windowCaptureMetadata[windowId] = metadata; debugLog(`Created basic metadata for window ${windowId}: ${JSON.stringify(metadata)}\n`, debug); } else { metadata = global.windowCaptureMetadata[windowId]; const age = Date.now() - metadata.timestamp; if (age > 5 * 60 * 1000) { debugLog(`Window capture metadata is stale (${(age/1000).toFixed(1)}s old), creating fresh basic metadata\n`, debug); // Refresh with basic metadata const activeWin = require('active-win'); const windows = await activeWin.getOpenWindows(); const targetWindow = windows.find(w => w && w.id === windowId); if (!targetWindow) { return { success: false, error: `Window not found with ID: ${windowId}` }; } metadata = { windowId: windowId, originalLogicalSize: { width: targetWindow.bounds.width, height: targetWindow.bounds.height }, aiImageSize: { width: targetWindow.bounds.width, height: targetWindow.bounds.height }, timestamp: Date.now() }; global.windowCaptureMetadata[windowId] = metadata; debugLog(`Refreshed basic metadata for window ${windowId}\n`, debug); } } // Get current window information (we might have it from metadata creation above) const activeWin = require('active-win'); const windows = await activeWin.getOpenWindows(); const targetWindow = windows.find(w => w && w.id === windowId); if (!targetWindow) { return { success: false, error: `Window not found for coordinate conversion with ID: ${windowId}` }; } // Title bar height removed - no longer needed for coordinate calculations // The scaling factor is the ratio of the window's original logical size // to the final size of the image that was sent to the AI. const scaleX = metadata.originalLogicalSize.width / metadata.aiImageSize.width; const scaleY = metadata.originalLogicalSize.height / metadata.aiImageSize.height; // Scale the AI's coordinates to find the position within the logical window let scaledX = Math.round(x * scaleX); let scaledY = Math.round(y * scaleY); // Add the window's origin to get the final screen coordinates. moveX = targetWindow.bounds.x + scaledX; moveY = targetWindow.bounds.y + scaledY; // Log the complete transformation for easy debugging const logTimestamp = new Date().toISOString(); debugLog(`\n=== MOUSE_MOVE_TRANSFORM ${logTimestamp} ===\n`, debug); debugLog(` - AI Coords: (${x}, ${y})\n`, debug); debugLog(` - AI Image Size: ${metadata.aiImageSize.width}x${metadata.aiImageSize.height}\n`, debug); debugLog(` - Logical Window Size: ${metadata.originalLogicalSize.width}x${metadata.originalLogicalSize.height}\n`, debug); debugLog(` - Scale Factors: X=${scaleX.toFixed(3)}, Y=${scaleY.toFixed(3)}\n`, debug); debugLog(` - Scaled Coords: (${scaledX}, ${scaledY})\n`, debug); debugLog(` - Window Origin: (${targetWindow.bounds.x}, ${targetWindow.bounds.y})\n`, debug); debugLog(` - Final Screen Coords: (${moveX}, ${moveY})\n`, debug); robot.moveMouse(moveX, moveY); if (debug) { const sharp = require('sharp'); const timestamp = Date.now(); // First, if windowInsideCoordinates, capture and save the window with intended click point try { debugLog(`DEBUG: Attempting to capture window ${windowId} for debug...\n`, debug); const windowCapture = await capabilityImplementations.window_capture({ windowId }); if (!windowCapture || windowCapture.isError) { debugLog(`DEBUG: Window capture failed: ${windowCapture ? windowCapture.error : 'null response'}\n`, debug); } else if (windowCapture.content && windowCapture.content[1]) { const windowImgBuffer = Buffer.from(windowCapture.content[1].data, 'base64'); // Get metadata to understand the image size const windowMeta = await sharp(windowImgBuffer).metadata(); debugLog(`DEBUG: Window image size: ${windowMeta.width}x${windowMeta.height}, format: ${windowMeta.format}\n`, debug); // Create a red circle at the intended click position const circleSize = 10; // The debug window image size should match the AI image size from metadata const scaleFactorForDebugX = windowMeta.width / metadata.aiImageSize.width; const scaleFactorForDebugY = windowMeta.height / metadata.aiImageSize.height; debugLog(`DEBUG: Using metadata scale - Window debug image: ${windowMeta.width}x${windowMeta.height}, AI saw: ${metadata.aiImageSize.width}x${metadata.aiImageSize.height}\n`, debug); const scaledX = Math.round(originalX * scaleFactorForDebugX); const scaledY = Math.round(originalY * scaleFactorForDebugY); debugLog(`DEBUG: Original AI coordinates: (${originalX}, ${originalY}), Scaled for window capture: (${scaledX}, ${scaledY})\n`, debug); debugLog(`DEBUG: Circle will be placed at top=${Math.round(scaledY - circleSize/2)}, left=${Math.round(scaledX - circleSize/2)}\n`, debug); const circle = Buffer.from( `<svg width="${circleSize}" height="${circleSize}"> <circle cx="${circleSize/2}" cy="${circleSize/2}" r="${circleSize/2-1}" fill="red" fill-opacity="0.7" stroke="darkred" stroke-width="2"/> <circle cx="${circleSize/2}" cy="${circleSize/2}" r="1" fill="white"/> </svg>` ); // Mark the intended position in the window capture with scaled coordinates const markedWindow = await sharp(windowImgBuffer) .composite([{ input: circle, top: Math.max(0, Math.round(scaledY - circleSize/2)), left: Math.max(0, Math.round(scaledX - circleSize/2)) }]) .jpeg({ quality: 80 }) .toBuffer(); // Save the marked window capture if (debug || isGlobalDebugEnabled) { const windowDebugPath = path.join(__dirname, `debug_window_${timestamp}.jpg`); fs.writeFileSync(windowDebugPath, markedWindow); debugLog(`DEBUG: Saved window capture with intended click at (${originalX}, ${originalY}) to ${windowDebugPath}\n`, debug); } // Also save without mark for comparison if (debug || isGlobalDebugEnabled) { const unmarkedPath = path.join(__dirname, `debug_window_unmarked_${timestamp}.jpg`); fs.writeFileSync(unmarkedPath, windowImgBuffer); debugLog(`DEBUG: Saved unmarked window capture to ${unmarkedPath}\n`, debug); } } else { debugLog(`DEBUG: Window capture response missing content\n`, debug); } } catch (err) { debugLog(`DEBUG: Error in window debug save: ${err.message}\n${err.stack}\n`, debug); } // Take a screenshot to show where the cursor is positioned const screenshot = await capabilityImplementations.screen_capture(); if (screenshot.isError === false) { // Get the actual cursor position from the system const actualCursorPos = robot.getMousePos(); // Use Sharp to draw a red dot at the actual cursor position const sharp = require('sharp'); const imgBuffer = Buffer.from(screenshot.content[1].data, 'base64'); // Get image metadata to handle scaling const metadata = await sharp(imgBuffer).metadata(); const screenSize = robot.getScreenSize(); // Calculate scaling factor const scaleX = metadata.width / screenSize.width; const scaleY = metadata.height / screenSize.height; // Scale actual cursor coordinates if needed const scaledX = Math.round(actualCursorPos.x * scaleX); const scaledY = Math.round(actualCursorPos.y * scaleY); // Create a red circle overlay const circleSize = 20; const circle = Buffer.from( `<svg width="${circleSize}" height="${circleSize}"> <circle cx="${circleSize/2}" cy="${circleSize/2}" r="${circleSize/2-2}" fill="none" stroke="red" stroke-width="3"/> <circle cx="${circleSize/2}" cy="${circleSize/2}" r="2" fill="red"/> </svg>` ); // Composite the circle onto the screenshot const annotatedImg = await sharp(imgBuffer) .composite([{ input: circle, top: Math.max(0, scaledY - circleSize/2), left: Math.max(0, scaledX - circleSize/2) }]) .jpeg({ quality: 80 }) .toBuffer(); const annotatedImgBase64 = annotatedImg.toString('base64'); // Save the full screenshot with actual cursor position if (debug || isGlobalDebugEnabled) { try { const fullScreenDebugPath = path.join(__dirname, `debug_fullscreen_${timestamp}.jpg`); await sharp(annotatedImg).toFile(fullScreenDebugPath); debugLog(`DEBUG: Saved full screenshot with actual cursor at (${actualCursorPos.x}, ${actualCursorPos.y}) to ${fullScreenDebugPath}\n`, debug); } catch (err) { // Silently ignore errors in debug saving } } const transformations = []; if (windowId) { const windows = await require('active-win').getOpenWindows(); const targetWindow = windows.find(w => w.id === windowId); if (targetWindow) { // Recalculate window-specific scaling for debug output const windowLogicalWidth = targetWindow.bounds.width; const windowLogicalHeight = targetWindow.bounds.height; const isHighDPI = (metadata.width / screenSize.width) > 1.5 || (metadata.height / screenSize.height) > 1.5; let windowPhysicalWidth, windowPhysicalHeight; if (isHighDPI) { const scaleX = metadata.width / screenSize.width; const scaleY = metadata.height / screenSize.height; windowPhysicalWidth = windowLogicalWidth * scaleX; windowPhysicalHeight = windowLogicalHeight * scaleY; } else { windowPhysicalWidth = windowLogicalWidth; windowPhysicalHeight = windowLogicalHeight; } // Target window size if resize was working (currently unused) // const targetWindowWidth = Math.min(Math.round(windowPhysicalWidth * 0.5), 1280); // const targetWindowHeight = Math.min(Math.round(windowPhysicalHeight * 0.5), 720); // Recalculate with correct AI window size for debug output const debugTargetWidth = Math.min(Math.round(windowPhysicalWidth * 0.5), 1280); const debugTargetHeight = Math.min(Math.round(windowPhysicalHeight * 0.5), 720); let debugAiWidth, debugAiHeight; if (debugTargetWidth < windowPhysicalWidth || debugTargetHeight < windowPhysicalHeight) { debugAiWidth = debugTargetWidth; debugAiHeight = debugTargetHeight; } else { debugAiWidth = windowPhysicalWidth; debugAiHeight = windowPhysicalHeight; } const debugScaleX = windowLogicalWidth / debugAiWidth; const debugScaleY = windowLogicalHeight / debugAiHeight; const step1X = Math.round(x * debugScaleX); const step1Y = Math.round(y * debugScaleY); transformations.push(`window AI coords (${x}, ${y}) in ${debugAiWidth}x${debugAiHeight} image -> window logical (${step1X}, ${step1Y})`); transformations.push(`window scale factors: X=${debugScaleX.toFixed(3)}, Y=${debugScaleY.toFixed(3)}`); transformations.push(`final screen coords (${step1X + targetWindow.bounds.x}, ${step1Y + targetWindow.bounds.y})`); } } else { const step1X = Math.round(x * scaleX); const step1Y = Math.round(y * scaleY); transformations.push(`AI coords (${x}, ${y}) -> logical coords (${step1X}, ${step1Y})`); transformations.push(`scale factors: X=${scaleX.toFixed(3)}, Y=${scaleY.toFixed(3)}`); } return { content: [ { type: "text", text: `Mouse moved to requested (${x}, ${y})${transformations.length > 0 ? ` -> ${transformations.join(' -> ')} -> final (${moveX}, ${moveY})` : ''}. Actual cursor position: (${actualCursorPos.x}, ${actualCursorPos.y}). Red circle shows actual cursor position for verification.`, }, { type: "image", mimeType: "image/jpeg", data: annotatedImgBase64, }, ], isError: false }; } } return { success: true }; } catch (error) { // Log any unexpected errors to the debug file for inspection const errorTimestamp = new Date().toISOString(); const { debug = false } = params; debugLog(`--- MOUSE_MOVE CRITICAL ERROR ${errorTimestamp} ---\n`, debug); debugLog(`Error: ${error.message}\n`, debug); debugLog(`Stack: ${error.stack}\n`, debug); debugLog(`-------------------------------------\n`, debug); // Error captured silently to avoid interfering with MCP protocol return { success: false, error: error.message }; } }, mouse_click: async (params = {}) => { try { const { button = 'left', double = false, windowId, pressLength = 0, x, y, debug = false } = params; // Add a timestamped log to mark the beginning of the function call const timestamp = new Date().toISOString(); debugLog(`\n--- MOUSE_CLICK START ${timestamp} ---\n`, debug); debugLog(`Params: ${JSON.stringify(params)}\n`, debug); // Validate pressLength if (pressLength < 0 || pressLength > 5000) { debugLog(`MOUSE_CLICK ERROR: Invalid pressLength\n`, debug); return { success: false, error: 'pressLength must be between 0 and 5000 milliseconds' }; } // Always focus window first if windowId is provided if (windowId) { debugLog(`MOUSE_CLICK: Focusing window ${windowId}\n`, debug); const focusResult = await capabilityImplementations.focus_window({ windowId }); if (!focusResult.success) { debugLog(`MOUSE_CLICK ERROR: Failed to focus window: ${focusResult.error}\n`, debug); return focusResult; } // Wait for window to focus await new Promise(resolve => setTimeout(resolve, 300)); } // If coordinates are provided, move mouse if (x !== undefined && y !== undefined) { debugLog(`MOUSE_CLICK: Calling mouse_move with x=${x}, y=${y}\n`, debug); const moveResult = await capabilityImplementations.mouse_move({ x, y, windowId, debug // Pass debug flag through }); if (!moveResult.success) { debugLog(`MOUSE_CLICK ERROR: mouse_move failed: ${moveResult.error}\n`, debug); return moveResult; } // Small delay after move to ensure position is set await new Promise(resolve => setTimeout(resolve, 50)); } debugLog(`MOUSE_CLICK: Performing click action (button: ${button}, double: ${double}, pressLength: ${pressLength})\n`, debug); if (pressLength > 0) { // Hold mouse button for specified duration robot.mouseToggle('down', button); await new Promise(resolve => setTimeout(resolve, pressLength)); robot.mouseToggle('up', button); } else { // Quick click (default behavior) if (double) { debugLog(`MOUSE_CLICK: Trying native robotjs double-click with robot.mouseClick('${button}', true)\n`, debug); // Try native robotjs double-click first robot.mouseClick(button, true); // If that doesn't work, fall back to two clicks // Uncomment these lines if native double-click doesn't work: // robot.mouseClick(button); // await new Promise(resolve => setTimeout(resolve, 200)); // robot.mouseClick(button); } else { debugLog(`MOUSE_CLICK: Executing single-click with robot.mouseClick('${button}')\n`, debug); robot.mouseClick(button); } } debugLog(`--- MOUSE_CLICK END ---\n`, debug); return { success: true }; } catch (error) { // Log any unexpected errors to the debug file for inspection const errorTimestamp = new Date().toISOString(); const { debug = false } = params; debugLog(`--- MOUSE_CLICK CRITICAL ERROR ${errorTimestamp} ---\n`, debug); debugLog(`Error: ${error.message}\n`, debug); debugLog(`Stack: ${error.stack}\n`, debug); debugLog(`-------------------------------------\n`, debug); // Error captured silently to avoid interfering with MCP protocol return { success: false, error: error.message }; } }, keyboard_type: async (params) => { try { const { text, windowId } = params; // Focus window if windowId is provided if (windowId) { const focusResult = await capabilityImplementations.focus_window({ windowId }); if (!focusResult.success) { return focusResult; } // Wait for window to focus await new Promise(resolve => setTimeout(resolve, 300)); } robot.typeString(text); return { success: true }; } catch (error) { // Error captured silently to avoid interfering with MCP protocol return { success: false, error: error.message }; } }, keyboard_press: async (params) => { try { const { key, modifiers = [], windowId, pressLength = 0 } = params; // Validate pressLength if (pressLength < 0 || pressLength > 5000) { return { success: false, error: 'pressLength must be between 0 and 5000 milliseconds' }; } // Focus window if windowId is provided if (windowId) { const focusResult = await capabilityImplementations.focus_window({ windowId }); if (!focusResult.success) { return focusResult; } // Wait for window to focus await new Promise(resolve => setTimeout(resolve, 300)); } if (pressLength > 0) { // Hold key for specified duration if (modifiers.length === 0) { robot.keyToggle(key, 'down'); await new Promise(resolve => setTimeout(resolve, pressLength)); robot.keyToggle(key, 'up'); } else { robot.keyToggle(key, 'down', modifiers); await new Promise(resolve => setTimeout(resolve, pressLength)); robot.keyToggle(key, 'up'); } } else { // Quick tap (default behavior) if (modifiers.length === 0) { robot.keyTap(key); } else { robot.keyToggle(key, 'down', modifiers); robot.keyToggle(key, 'up'); } } return { success: true }; } catch (error) { // Error captured silently to avoid interfering with MCP protocol return { success: false, error: error.message }; } }, get_screen_size: () => { try { const size = robot.getScreenSize(); return { success: true, result: size }; } catch (error) { // Error captured silently to avoid interfering with MCP protocol return { success: false, error: error.message }; } }, list_windows: async () => { try { const activeWin = require('active-win'); const windows = await activeWin.getOpenWindows(); if (!windows || !Array.isArray(windows)) { return { success: false, error: 'Failed to get window list - activeWin.getOpenWindows() returned null or invalid data' }; } // Get primary screen dimensions for display detection const primaryScreen = robot.getScreenSize(); return { success: true, result: windows.filter(window => window && window.id).map(window => { const bounds = window.bounds || { x: 0, y: 0, width: 0, height: 0 }; // Determine if window is on primary display or secondary display const isOnPrimaryDisplay = bounds.x >= 0 && bounds.y >= 0 && bounds.x < primaryScreen.width && bounds.y < primaryScreen.height; // Determine display location let displayLocation = 'primary'; if (!isOnPrimaryDisplay) { if (bounds.x < 0) { displayLocation = 'secondary-left'; } else if (bounds.x >= primaryScreen.width) { displayLocation = 'secondary-right'; } else if (bounds.y < 0) { displayLocation = 'secondary-top'; } else if (bounds.y >= primaryScreen.height) { displayLocation = 'secondary-bottom'; } else { displayLocation = 'secondary-unknown'; } } // Determine accessibility for mouse/keyboard actions // Note: Mouse movement works across all displays when using windowInsideCoordinates const isAccessibleForMouse = true; // Mouse works across displays with window-relative coordinates const isAccessibleForWindowActions = true; // focus_window and window_capture work across displays return { id: window.id, title: window.title || 'Untitled', owner: window.owner?.name || 'Unknown', bounds: bounds, processId: window.owner?.processId || 0, bundleId: window.owner?.bundleId || '', path: window.owner?.path || '', url: window.url || '', memoryUsage: window.memoryUsage || 0, displayLocation: displayLocation, isOnPrimaryDisplay: isOnPrimaryDisplay, accessibility: { mouseActions: isAccessibleForMouse, windowActions: isAccessibleForWindowActions, note: isOnPrimaryDisplay ? 'All coordinate systems available' : 'Use windowInsideCoordinates for reliable cross-display mouse control' } }; }) }; } catch (error) { // Error captured silently to avoid interfering with MCP protocol return { success: false, error: error.message }; } }, move_window_to_primary_screen: async (params) => { try { const activeWin = require('active-win'); const { windowId, preserveSize = true } = params; // Get all windows to find the target window const windows = await activeWin.getOpenWindows(); if (!windows || !Array.isArray(windows)) { return { success: false, error: 'Failed to get window list - activeWin.getOpenWindows() returned null or invalid data' }; } const targetWindow = windows.find(w => w && w.id === windowId); if (!targetWindow) { return { success: false, error: `Window not found with ID: ${windowId}` }; } // Check if window is already on primary display const primaryScreen = robot.getScreenSize(); const bounds = targetWindow.bounds; const isOnPrimaryDisplay = bounds.x >= 0 && bounds.y >= 0 && bounds.x < primaryScreen.width && bounds.y < primaryScreen.height; if (isOnPrimaryDisplay) { return { success: true, message: `Window ${windowId} is already on the primary screen` }; } // Calculate new position on primary screen // Place window at center-left of primary screen if preserveSize is true let newX, newY, newWidth, newHeight; if (preserveSize) { // Keep original size, center on primary screen newX = Math.max(0, Math.min(50, primaryScreen.width - bounds.width)); newY = Math.max(0, Math.min(50, primaryScreen.height - bounds.height)); newWidth = Math.min(bounds.width, primaryScreen.width - newX); newHeight = Math.min(bounds.height, primaryScreen.height - newY); } else { // Resize to fit primary screen comfortably newX = 100; newY = 100; newWidth = Math.min(bounds.width, primaryScreen.width - 200); newHeight = Math.min(bounds.height, primaryScreen.height - 200); } // Use cross-platform window helper const moveResult = await windowHelpers.moveWindow(targetWindow, newX, newY, newWidth, newHeight); if (moveResult.success) { // Wait a moment for the window to move await new Promise(resolve => setTimeout(resolve, 500)); return { success: true, message: `Window ${windowId} moved to primary screen at position (${newX}, ${newY}) with size ${newWidth}x${newHeight}`, newBounds: { x: newX, y: newY, width: newWidth, height: newHeight } }; } else { return moveResult; } } catch (error) { // Error captured silently to avoid interfering with MCP protocol return { success: false, error: error.message }; } }, focus_window: async (params) => { try { const activeWin = require('active-win'); const { windowId } = params; // Get all windows to find the target window const windows = await activeWin.getOpenWindows(); if (!windows || !Array.isArray(windows)) { return { success: false, error: 'Failed to get window list - activeWin.getOpenWindows() returned null or invalid data' }; } const targetWindow = windows.find(w => w && w.id === windowId); if (!targetWindow) { return { success: false, error: `Window not found with ID: ${windowId}` }; } // Use cross-platform window helper const focusResult = await windowHelpers.focusWindow(targetWindow); if (focusResult.success) { return { success: true, message: `Window ${windowId} focused successfully` }; } else { return focusResult; } } catch (error) { // Error captured silently to avoid interfering with MCP protocol return { success: false, error: error.message }; } }, window_capture: async (params) => { try { const activeWin = require('active-win'); const { windowId, windowTitle, debug = false } = params; let targetWindow; // Find the window by ID or title const windows = await activeWin.getOpenWindows(); if (!windows || !Array.isArray(windows)) { return { success: false, error: 'Failed to get window list - activeWin.getOpenWindows() returned null or invalid data' }; } if (windowId) { targetWindow = windows.find(w => w && w.id === windowId); } else if (windowTitle) { targetWindow = windows.find(w => w && w.title && w.title.toLowerCase().includes(windowTitle.toLowerCase())); } else { return { success: false, error: 'Either windowId or windowTitle must be provided' }; } if (!targetWindow) { return { success: false, error: `Window not found${windowId ? ` with ID: ${windowId}` : windowTitle ? ` with title containing: "${windowTitle}"` : ''}` }; } // Use cross-platform window help