Skip to main content
Glama

MCP Android Agent

by nim444
server.py22.5 kB
# server.py from mcp.server.fastmcp import FastMCP import uiautomator2 as u2 from typing import List, Optional, Dict, Any import shutil import subprocess import asyncio from typing import TypedDict # Create an MCP server mcp = FastMCP("MCP Server Android") # Type definitions for better type hints class DeviceInfo(TypedDict): manufacturer: str model: str serial: str version: str sdk: int display: str product: str class AppInfo(TypedDict): package_name: str version_name: str version_code: int first_install_time: str last_update_time: str class ElementInfo(TypedDict): text: str resourceId: str description: str className: str enabled: bool clickable: bool bounds: Dict[str, Any] selected: bool focused: bool @mcp.tool(name="mcp_health", description="Simple mcp health tool") def mcp_health() -> str: return "Hello, world!" @mcp.tool( name="connect_device", description="Connect to an Android device and return device info with uiautomator2", ) def connect_device(device_id: Optional[str] = None) -> DeviceInfo: """Connect to an Android device and get its information. Args: device_id: Optional device ID to connect to Returns: Device information including manufacturer, model, etc. """ try: d = u2.connect(device_id) info = d.info return { "manufacturer": info.get("manufacturer", ""), "model": info.get("model", ""), "serial": info.get("serial", ""), "version": info.get("version", {}).get("release", ""), "sdk": info.get("version", {}).get("sdk", 0), "display": info.get("display", {}).get("density", ""), "product": info.get("productName", ""), } except Exception as e: raise ConnectionError( f"Failed to connect to device with ID '{device_id}': {str(e)}" ) @mcp.tool(name="get_installed_apps", description="List installed apps") def get_installed_apps(device_id: Optional[str] = None) -> List[AppInfo]: """Get a list of all installed applications on the device. Args: device_id: Optional device ID to connect to Returns: List of application information including package names and versions """ d = u2.connect(device_id) return d.app_list() @mcp.tool(name="get_current_app", description="Get info about the foreground app") def get_current_app(device_id: Optional[str] = None) -> AppInfo: """Get information about the currently active application. Args: device_id: Optional device ID to connect to Returns: Information about the current foreground application """ d = u2.connect(device_id) return d.app_current() @mcp.tool(name="start_app", description="Start an app by package name") def start_app( package_name: str, device_id: Optional[str] = None, wait: bool = True ) -> bool: """Start an application by its package name. Args: package_name: The package name of the application to start device_id: Optional device ID to connect to wait: Whether to wait for the app to come to the foreground Returns: True if the app was started successfully, False otherwise """ try: d = u2.connect(device_id) d.app_start(package_name) if wait: pid = d.app_wait(package_name, front=True) return pid is not None return True except Exception as e: print(f"Failed to start app {package_name}: {str(e)}") return False @mcp.tool(name="stop_app", description="Stop an app by package name") def stop_app(package_name: str, device_id: Optional[str] = None) -> bool: """Stop an application by its package name. Args: package_name: The package name of the application to stop device_id: Optional device ID to connect to Returns: True if the app was stopped successfully, False otherwise """ try: d = u2.connect(device_id) d.app_stop(package_name) return True except Exception as e: print(f"Failed to stop app {package_name}: {str(e)}") return False @mcp.tool(name="stop_all_apps", description="Stop all running apps") def stop_all_apps(device_id: Optional[str] = None) -> bool: """Stop all running applications on the device. Args: device_id: Optional device ID to connect to Returns: True if all apps were stopped successfully, False otherwise """ try: d = u2.connect(device_id) d.app_stop_all() return True except Exception as e: print(f"Failed to stop all apps: {str(e)}") return False @mcp.tool(name="screen_on", description="Turn screen on") def screen_on(device_id: Optional[str] = None) -> bool: """Turn the device screen on. Args: device_id: Optional device ID to connect to Returns: True if the screen was turned on successfully, False otherwise """ try: d = u2.connect(device_id) d.screen_on() return True except Exception as e: print(f"Failed to turn screen on: {str(e)}") return False @mcp.tool(name="screen_off", description="Turn screen off") def screen_off(device_id: Optional[str] = None) -> bool: """Turn the device screen off. Args: device_id: Optional device ID to connect to Returns: True if the screen was turned off successfully, False otherwise """ try: d = u2.connect(device_id) d.screen_off() return True except Exception as e: print(f"Failed to turn screen off: {str(e)}") return False @mcp.tool(name="get_device_info", description="Get detailed device information") def get_device_info(device_id: Optional[str] = None) -> Dict[str, Any]: """Get detailed information about the device. Args: device_id: Optional device ID to connect to Returns: A dictionary containing detailed device information """ try: d = u2.connect(device_id) info = d.info display = d.window_size() return { "serial": d.serial, "resolution": f"{display[0]}x{display[1]}", "version": info.get("version", {}).get("release", ""), "sdk": info.get("version", {}).get("sdk", 0), "battery": d.battery_info, "wifi_ip": d.wlan_ip, "manufacturer": info.get("manufacturer", ""), "model": info.get("model", ""), "is_screen_on": d.screen_on(), } except Exception as e: print(f"Failed to get device info: {str(e)}") return {} @mcp.tool(name="press_key", description="Press a key on the device") def press_key(key: str, device_id: Optional[str] = None) -> bool: """Press a key on the device. Args: key: The key to press (e.g., 'home', 'back', 'menu', etc.) device_id: Optional device ID to connect to Returns: True if the key was pressed successfully, False otherwise """ try: d = u2.connect(device_id) d.press(key) return True except Exception as e: print(f"Failed to press key {key}: {str(e)}") return False @mcp.tool(name="unlock_screen", description="Unlock the device screen") def unlock_screen(device_id: Optional[str] = None) -> bool: """Unlock the device screen. Args: device_id: Optional device ID to connect to Returns: True if the screen was unlocked successfully, False otherwise """ try: d = u2.connect(device_id) if not d.info["screenOn"]: d.screen_on() d.unlock() return True except Exception as e: print(f"Failed to unlock screen: {str(e)}") return False @mcp.tool(name="check_adb", description="Check ADB and list devices") def check_adb_and_list_devices() -> Dict[str, Any]: """Check if ADB is available and list connected devices. Returns: A dictionary containing ADB status and connected devices """ adb_path = shutil.which("adb") if not adb_path: return { "adb_exists": False, "devices": [], "error": "adb command not found in PATH", } try: result = subprocess.run( [adb_path, "devices"], capture_output=True, text=True, check=True ) lines = result.stdout.strip().splitlines() devices = [] for line in lines[1:]: if line.strip(): parts = line.split() if len(parts) >= 2 and parts[1] == "device": devices.append(parts[0]) return {"adb_exists": True, "devices": devices, "error": None} except Exception as e: return {"adb_exists": True, "devices": [], "error": str(e)} @mcp.tool(name="wait_for_screen_on", description="Wait until device screen is on") async def wait_for_screen_on(device_id: str) -> str: """Wait until the device screen is turned on. Args: device_id: The device ID to connect to Returns: A message indicating the screen is now on """ d = u2.connect(device_id) while not d.screen_on(): await asyncio.sleep(1) return "Screen is now on" @mcp.tool( name="click", description="Click on an element by text, description, or resource ID" ) def click( selector: str, selector_type: str = "text", timeout: float = 10.0, device_id: Optional[str] = None, ) -> bool: """Click on an element on the device screen. Args: selector: The selector to identify the element selector_type: The type of selector ('text', 'resourceId', 'description') timeout: Timeout for waiting for the element device_id: Optional device ID to connect to Returns: True if the element was clicked successfully, False otherwise """ try: d = u2.connect(device_id) if selector_type == "text": el = d(text=selector).wait(timeout=timeout) elif selector_type == "resourceId": el = d(resourceId=selector).wait(timeout=timeout) elif selector_type == "description": el = d(description=selector).wait(timeout=timeout) else: raise ValueError(f"Invalid selector_type: {selector_type}") if el and el.exists: el.click() return True return False except Exception as e: print(f"Failed to click element {selector}: {str(e)}") return False @mcp.tool( name="send_text", description="Send text to current focused element or clear and send if clear=True", ) def send_text(text: str, clear: bool = True, device_id: Optional[str] = None) -> bool: """Send text to the currently focused element on the device. Args: text: The text to send clear: Whether to clear the existing text before sending device_id: Optional device ID to connect to Returns: True if the text was sent successfully, False otherwise """ try: d = u2.connect(device_id) d.send_keys(text, clear=clear) return True except Exception as e: print(f"Failed to send text: {str(e)}") return False @mcp.tool( name="get_element_info", description="Get information about an element by text, description, or resource ID", ) def get_element_info( selector: str, selector_type: str = "text", timeout: float = 10.0, device_id: Optional[str] = None, ) -> ElementInfo: """Get information about an element on the device screen. Args: selector: The selector to identify the element selector_type: The type of selector ('text', 'resourceId', 'description') timeout: Timeout for waiting for the element device_id: Optional device ID to connect to Returns: A dictionary containing information about the element """ try: d = u2.connect(device_id) if selector_type == "text": el = d(text=selector).wait(timeout=timeout) elif selector_type == "resourceId": el = d(resourceId=selector).wait(timeout=timeout) elif selector_type == "description": el = d(description=selector).wait(timeout=timeout) else: raise ValueError(f"Invalid selector_type: {selector_type}") if el and el.exists: info = el.info return { "text": info.get("text", ""), "resourceId": info.get("resourceId", ""), "description": info.get("contentDescription", ""), "className": info.get("className", ""), "enabled": info.get("enabled", False), "clickable": info.get("clickable", False), "bounds": info.get("bounds", {}), "selected": info.get("selected", False), "focused": info.get("focused", False), } return {} except Exception as e: print(f"Failed to get element info for {selector}: {str(e)}") return {} @mcp.tool(name="swipe", description="Perform a swipe gesture from one point to another") def swipe( start_x: float, start_y: float, end_x: float, end_y: float, duration: float = 0.5, device_id: Optional[str] = None, ) -> bool: """Perform a swipe gesture on the device screen. Args: start_x: Starting X coordinate start_y: Starting Y coordinate end_x: Ending X coordinate end_y: Ending Y coordinate duration: Duration of the swipe device_id: Optional device ID to connect to Returns: True if the swipe was performed successfully, False otherwise """ try: d = u2.connect(device_id) d.swipe(start_x, start_y, end_x, end_y, duration=duration) return True except Exception as e: print(f"Failed to perform swipe: {str(e)}") return False @mcp.tool( name="wait_for_element", description="Wait for an element to appear on screen" ) def wait_for_element( selector: str, selector_type: str = "text", timeout: float = 10.0, device_id: Optional[str] = None, ) -> bool: """Wait for an element to appear on the device screen. Args: selector: The selector to identify the element selector_type: The type of selector ('text', 'resourceId', 'description') timeout: Timeout for waiting for the element device_id: Optional device ID to connect to Returns: True if the element appeared successfully, False otherwise """ try: d = u2.connect(device_id) if selector_type == "text": return d(text=selector).wait(timeout=timeout) elif selector_type == "resourceId": return d(resourceId=selector).wait(timeout=timeout) elif selector_type == "description": el = d(description=selector).wait(timeout=timeout) return el is not None and el.exists else: raise ValueError(f"Invalid selector_type: {selector_type}") except Exception as e: print(f"Failed to wait for element {selector}: {str(e)}") return False @mcp.tool( name="screenshot", description="Take a screenshot and save it to the specified path" ) def screenshot(filename: str, device_id: Optional[str] = None) -> bool: """Take a screenshot of the device screen. Args: filename: The file path to save the screenshot device_id: Optional device ID to connect to Returns: True if the screenshot was taken successfully, False otherwise """ try: d = u2.connect(device_id) d.screenshot(filename) return True except Exception as e: print(f"Failed to take screenshot: {str(e)}") return False @mcp.tool(name="long_click", description="Long click on an element") def long_click( selector: str, selector_type: str = "text", duration: float = 1.0, device_id: Optional[str] = None, ) -> bool: """Perform a long click on an element on the device screen. Args: selector: The selector to identify the element selector_type: The type of selector ('text', 'resourceId', 'description') duration: Duration of the long click device_id: Optional device ID to connect to Returns: True if the long click was performed successfully, False otherwise """ try: d = u2.connect(device_id) if selector_type == "text": el = d(text=selector) elif selector_type == "resourceId": el = d(resourceId=selector) elif selector_type == "description": el = d(description=selector) else: raise ValueError(f"Invalid selector_type: {selector_type}") if el and el.exists: el.long_click(duration=duration) return True return False except Exception as e: print(f"Failed to long click element {selector}: {str(e)}") return False @mcp.tool(name="scroll_to", description="Scroll to an element") def scroll_to( selector: str, selector_type: str = "text", device_id: Optional[str] = None ) -> bool: """Scroll to an element on the device screen. Args: selector: The selector to identify the element selector_type: The type of selector ('text', 'resourceId', 'description') device_id: Optional device ID to connect to Returns: True if the scroll was performed successfully, False otherwise """ try: d = u2.connect(device_id) if selector_type == "text": return d(scrollable=True).scroll.to(text=selector) elif selector_type == "resourceId": return d(scrollable=True).scroll.to(resourceId=selector) elif selector_type == "description": el = d(scrollable=True).scroll.to(description=selector) return el is not None and el.exists else: raise ValueError(f"Invalid selector_type: {selector_type}") except Exception as e: print(f"Failed to scroll to element {selector}: {str(e)}") return False @mcp.tool(name="drag", description="Drag an element to a specific location") def drag( selector: str, selector_type: str, to_x: int, to_y: int, device_id: Optional[str] = None, ) -> bool: """Drag an element to a specific location on the device screen. Args: selector: The selector to identify the element selector_type: The type of selector ('text', 'resourceId', 'description') to_x: The X coordinate to drag to to_y: The Y coordinate to drag to device_id: Optional device ID to connect to Returns: True if the drag was performed successfully, False otherwise """ try: d = u2.connect(device_id) if selector_type == "text": el = d(text=selector) elif selector_type == "resourceId": el = d(resourceId=selector) elif selector_type == "description": el = d(description=selector) else: raise ValueError(f"Invalid selector_type: {selector_type}") if el and el.exists: el.drag_to(to_x, to_y) return True return False except Exception as e: print(f"Failed to drag element {selector}: {str(e)}") return False @mcp.tool(name="get_toast", description="Get the text of the last toast message") def get_toast(device_id: Optional[str] = None) -> str: """Get the text of the last toast message displayed on the device. Args: device_id: Optional device ID to connect to Returns: The text of the last toast message """ try: d = u2.connect(device_id) return d.toast.get_message(10.0) or "" except Exception as e: print(f"Failed to get toast message: {str(e)}") return "" @mcp.tool(name="clear_app_data", description="Clear an app's data") def clear_app_data(package_name: str, device_id: Optional[str] = None) -> bool: """Clear the data of an application on the device. Args: package_name: The package name of the application device_id: Optional device ID to connect to Returns: True if the app data was cleared successfully, False otherwise """ try: d = u2.connect(device_id) d.app_clear(package_name) return True except Exception as e: print(f"Failed to clear app data for {package_name}: {str(e)}") return False @mcp.tool(name="wait_activity", description="Wait for a specific activity to appear") def wait_activity( activity: str, timeout: float = 10.0, device_id: Optional[str] = None ) -> bool: """Wait for a specific activity to appear on the device. Args: activity: The name of the activity to wait for timeout: Timeout for waiting for the activity device_id: Optional device ID to connect to Returns: True if the activity appeared successfully, False otherwise """ try: d = u2.connect(device_id) return d.wait_activity(activity, timeout=timeout) except Exception as e: print(f"Failed to wait for activity {activity}: {str(e)}") return False @mcp.tool( name="dump_hierarchy", description="Dump the UI hierarchy of the current screen" ) def dump_hierarchy( compressed: bool = False, pretty: bool = True, max_depth: int = 50, device_id: Optional[str] = None, ) -> str: """Dump the UI hierarchy of the current screen. Args: compressed: Whether to include not important nodes (False to include all nodes) pretty: Whether to format the output XML max_depth: Maximum depth of the XML hierarchy to include device_id: Optional device ID to connect to Returns: XML string representation of the UI hierarchy """ try: d = u2.connect(device_id) xml = d.dump_hierarchy( compressed=compressed, pretty=pretty, max_depth=max_depth ) return xml except Exception as e: print(f"Failed to dump UI hierarchy: {str(e)}") return "" if __name__ == "__main__": # using uvicorn to run the server mcp.run(transport="stdio")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nim444/mcp-android-server-python'

If you have feedback or need assistance with the MCP directory API, please join our Discord server