AiDD MCP Server

import json import platform import subprocess from typing import Any, Dict, List from mcp import types # Import platform-specific libraries if available try: import Quartz QUARTZ_AVAILABLE = True except ImportError: QUARTZ_AVAILABLE = False try: import pygetwindow as gw PYGETWINDOW_AVAILABLE = True except ImportError: PYGETWINDOW_AVAILABLE = False def get_active_apps_tool(): """Define the get_active_apps tool.""" return { "name": "get_active_apps", "description": "Get a list of currently active applications running on the user's system. " "This tool retrieves applications that have visible windows or are currently running. " "The response provides detailed information about running applications, which can help " "with context-aware assistance, troubleshooting, or when you need to know what software " "the user is currently working with.", "inputSchema": { "type": "object", "properties": { "with_details": { "type": "boolean", "description": "Whether to include additional details about each application, such as process IDs, window status, etc. Default is False." } }, "required": [] }, } def _get_active_apps_macos(with_details: bool = False) -> List[Dict[str, Any]]: """Get a list of currently active applications on macOS.""" active_apps = [] # Attempt to use Quartz directly first, as it's more reliable if QUARTZ_AVAILABLE: try: window_list = Quartz.CGWindowListCopyWindowInfo( Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID ) # Create a map of app names to their details app_map = {} for window in window_list: owner = window.get('kCGWindowOwnerName', '') # Skip empty app names or system components we don't want to include if not owner or owner in ["SystemUIServer", "osascript"]: continue # Create new entry for this app if we haven't seen it before if owner not in app_map: app_map[owner] = { "name": owner, "has_windows": False, "window_count": 0, "visible_windows": [] if with_details else None } # Count this window app_map[owner]["window_count"] += 1 # Check if this is a visible application window layer = window.get('kCGWindowLayer', 999) name = window.get('kCGWindowName', '') # Layer 0 typically indicates a standard application window if layer <= 0: app_map[owner]["has_windows"] = True # Add details about this window if detailed info was requested if with_details and name: app_map[owner]["visible_windows"].append({ "name": name, "id": window.get('kCGWindowNumber', 0) }) # Convert the map to a list active_apps = list(app_map.values()) # If we got results from Quartz, we're done if active_apps: return active_apps except Exception as e: print(f"Error getting applications with Quartz: {str(e)}") # Fall back to AppleScript if Quartz failed or isn't available if not active_apps: try: # Modified AppleScript that tries to avoid including itself script = ''' tell application "System Events" set appList to {} set allProcesses to application processes whose background only is false repeat with proc in allProcesses set procName to name of proc set procVisible to (windows of proc is not {}) # Skip the scripting process itself if procName is not "osascript" and procName is not "System Events" then set end of appList to {name:procName, has_windows:procVisible} end if end repeat return appList end tell ''' result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True) if result.returncode == 0: # Parse the output from AppleScript output = result.stdout.strip() if output: # AppleScript returns a list of records, we need to parse it lines = output.split(", {") for i, line in enumerate(lines): if i == 0: line = line.replace("{", "") if i == len(lines) - 1: line = line.replace("}", "") if "name:" in line and "has_windows:" in line: parts = line.split(", ") app_info = {} for part in parts: if "name:" in part: app_info["name"] = part.replace("name:", "").strip() elif "has_windows:" in part: app_info["has_windows"] = part.replace("has_windows:", "").strip().lower() == "true" if app_info: active_apps.append(app_info) except Exception as e: print(f"Error getting apps with AppleScript: {str(e)}") # Add window details if requested and if we got results from AppleScript if active_apps and with_details and QUARTZ_AVAILABLE: try: window_list = Quartz.CGWindowListCopyWindowInfo( Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID ) # Create a map of app names to window details app_details = {} for window in window_list: owner = window.get('kCGWindowOwnerName', '') if not owner: continue if owner not in app_details: app_details[owner] = { "window_count": 0, "windows": [] } app_details[owner]["window_count"] += 1 # Add window details if this is a visible window layer = window.get('kCGWindowLayer', 999) name = window.get('kCGWindowName', '') if layer <= 0 and name: app_details[owner]["windows"].append({ "name": name, "id": window.get('kCGWindowNumber', 0) }) # Enhance the active_apps list with these details for app in active_apps: app_name = app["name"] if app_name in app_details: app["window_count"] = app_details[app_name]["window_count"] app["visible_windows"] = app_details[app_name]["windows"] except Exception as e: print(f"Error getting window details with Quartz: {str(e)}") return active_apps def _get_active_apps_windows(with_details: bool = False) -> List[Dict[str, Any]]: """Get a list of currently active applications on Windows.""" active_apps = [] # Basic list without details if not with_details: try: # Use a PowerShell command to get running applications script = ''' Get-Process | Where-Object {$_.MainWindowTitle -ne ""} | Select-Object ProcessName, MainWindowTitle | ConvertTo-Json ''' cmd = ["powershell", "-Command", script] process = subprocess.run(cmd, capture_output=True, text=True) if process.returncode == 0: try: apps_data = json.loads(process.stdout) # Handle single item (not in a list) if isinstance(apps_data, dict): apps_data = [apps_data] for app in apps_data: active_apps.append({ "name": app.get("ProcessName", ""), "has_windows": True, "window_title": app.get("MainWindowTitle", "") }) except json.JSONDecodeError: print("Failed to parse JSON from PowerShell output") except Exception as e: print(f"Error getting basic app list on Windows: {str(e)}") # More detailed list with PyGetWindow if available elif PYGETWINDOW_AVAILABLE: try: # Get the list of windows all_windows = gw.getAllWindows() # Group by application (approximate, since we only have window titles) app_windows = {} for window in all_windows: if not window.title: continue # Try to extract application name from window title # This is an approximation and might not be accurate for all applications title_parts = window.title.split(' - ') app_name = title_parts[-1] if len(title_parts) > 1 else window.title if app_name not in app_windows: app_windows[app_name] = { "name": app_name, "has_windows": True, "window_count": 0, "visible_windows": [] } app_windows[app_name]["window_count"] += 1 app_windows[app_name]["visible_windows"].append({ "name": window.title, "width": window.width, "height": window.height }) active_apps = list(app_windows.values()) except Exception as e: print(f"Error getting detailed app list with PyGetWindow: {str(e)}") # Fallback to a basic PowerShell approach if not active_apps: try: script = ''' Get-Process | Where-Object {$_.MainWindowHandle -ne 0} | Select-Object ProcessName | ConvertTo-Json ''' cmd = ["powershell", "-Command", script] process = subprocess.run(cmd, capture_output=True, text=True) if process.returncode == 0: try: apps_data = json.loads(process.stdout) # Handle single item (not in a list) if isinstance(apps_data, dict): apps_data = [apps_data] for app in apps_data: active_apps.append({ "name": app.get("ProcessName", ""), "has_windows": True }) except json.JSONDecodeError: print("Failed to parse JSON from PowerShell output") except Exception as e: print(f"Error getting fallback app list on Windows: {str(e)}") return active_apps def _get_active_apps_linux(with_details: bool = False) -> List[Dict[str, Any]]: """Get a list of currently active applications on Linux.""" active_apps = [] # Try using wmctrl if available try: # Check if wmctrl is installed check_process = subprocess.run(["which", "wmctrl"], capture_output=True) if check_process.returncode == 0: # Get window list with wmctrl wmctrl_process = subprocess.run(["wmctrl", "-l"], capture_output=True, text=True) if wmctrl_process.returncode == 0: window_data = wmctrl_process.stdout.strip().split('\n') # Process each window line app_windows = {} for line in window_data: if not line: continue parts = line.split(None, 3) # Split by whitespace, max 3 splits if len(parts) >= 4: window_id, desktop, host, title = parts # Try to determine app name from window title app_name = title.split(' - ')[-1] if ' - ' in title else title if app_name not in app_windows: app_windows[app_name] = { "name": app_name, "has_windows": True, "window_count": 0, "visible_windows": [] } app_windows[app_name]["window_count"] += 1 if with_details: app_windows[app_name]["visible_windows"].append({ "name": title, "id": window_id, "desktop": desktop }) active_apps = list(app_windows.values()) except Exception as e: print(f"Error getting apps with wmctrl: {str(e)}") # If wmctrl failed or isn't available, try using ps if not active_apps: try: # List GUI applications cmd = ["ps", "-e", "-o", "comm="] process = subprocess.run(cmd, capture_output=True, text=True) if process.returncode == 0: all_processes = process.stdout.strip().split('\n') # Filter for likely GUI applications (very basic heuristic) gui_indicators = ["-bin", "x11", "gtk", "qt", "wayland", "gnome", "kde"] for proc in all_processes: proc = proc.strip() if not proc: continue # Skip system processes that typically don't have UIs if proc.startswith(("ps", "bash", "sh", "zsh", "systemd", "login", "dbus")): continue # Include if it looks like a GUI app if any(indicator in proc.lower() for indicator in gui_indicators) or "/" not in proc: active_apps.append({ "name": proc, "has_windows": True # Assuming these have windows, though we can't be sure }) except Exception as e: print(f"Error getting apps with ps: {str(e)}") return active_apps def get_active_apps(with_details: bool = False) -> Dict[str, Any]: """ Get a list of currently active applications on the user's system. Args: with_details: Whether to include additional details about each application Returns: Dictionary with platform, success status, and list of active applications """ system_name = platform.system().lower() # Get active apps based on platform if system_name == "darwin" or system_name == "macos": active_apps = _get_active_apps_macos(with_details) elif system_name == "windows": active_apps = _get_active_apps_windows(with_details) elif system_name == "linux": active_apps = _get_active_apps_linux(with_details) else: return { "success": False, "platform": system_name, "error": f"Unsupported platform: {system_name}. This tool currently supports macOS, Windows, and Linux.", "apps": [] } # If no apps were found, provide a descriptive error message if not active_apps: error_message = "No active applications could be detected. " if system_name == "darwin": error_message += ("This is most likely due to missing screen recording permissions. " "Please go to System Settings > Privacy & Security > Screen Recording " "and ensure that your terminal or IDE application has permission to record the screen.") elif system_name == "windows": error_message += "This might be due to insufficient permissions or no applications with visible windows." elif system_name == "linux": error_message += "This might be due to wmctrl not being installed or no applications with visible windows." return { "success": False, "platform": system_name, "error": error_message, "apps": [] } # Sort by name active_apps.sort(key=lambda app: app.get("name", "").lower()) return { "success": True, "platform": system_name, "app_count": len(active_apps), "apps": active_apps } async def handle_get_active_apps(arguments: dict) -> List[types.TextContent]: """Handle getting active applications.""" with_details = arguments.get("with_details", False) result = get_active_apps(with_details) return [types.TextContent(type="text", text=json.dumps(result, indent=2))]