android-app
Manage Android applications on devices: install, uninstall, start, stop, clear data, list packages, and retrieve app information through DroidMind MCP server.
Instructions
Perform various application management operations on an Android device.
This single tool consolidates various app-related actions. The 'action' parameter determines the operation.
Args:
serial: Device serial number.
action: The specific app operation to perform.
ctx: MCP Context for logging and interaction.
package (Optional[str]): Package name for the target application. Required by most actions.
apk_path (Optional[str]): Path to the APK file (local to the server). Used by install_app.
reinstall (Optional[bool]): Whether to reinstall if app exists. Used by install_app.
grant_permissions (Optional[bool]): Whether to grant all requested permissions. Used by install_app.
keep_data (Optional[bool]): Whether to keep app data and cache directories. Used by uninstall_app.
activity (Optional[str]): Optional activity name to start. Used by start_app.
extras (Optional[dict[str, str]]): Optional intent extras. Used by start_intent.
include_system_apps (Optional[bool]): Whether to include system apps. Used by list_packages.
include_app_name (Optional[bool]): Whether to include app labels (best-effort). Used by list_packages.
include_apk_path (Optional[bool]): Whether to include APK paths. Used by list_packages.
max_packages (Optional[int]): Max packages to return. Used by list_packages.
Returns: A string message indicating the result or status of the operation.
Available Actions and their specific argument usage:
action="install_app"Requires:
apk_pathOptional:
reinstall,grant_permissions
action="uninstall_app"Requires:
packageOptional:
keep_data
action="start_app"Requires:
packageOptional:
activity3b.action="start_intent"Requires:
package,activityOptional:
extras
action="stop_app"Requires:
package
action="clear_app_data"Requires:
package
action="list_packages"Optional:
include_system_apps,include_app_name,include_apk_path,max_packages
action="get_app_manifest"Requires:
package
action="get_app_permissions"Requires:
package
action="get_app_activities"Requires:
package
action="get_app_info"Requires:
package
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| serial | Yes | ||
| action | Yes | ||
| package | No | ||
| apk_path | No | ||
| reinstall | No | ||
| grant_permissions | No | ||
| keep_data | No | ||
| activity | No | ||
| extras | No | ||
| include_system_apps | No | ||
| include_app_name | No | ||
| include_apk_path | No | ||
| max_packages | No |
Implementation Reference
- Main handler for 'android-app' tool. Decorated with @mcp.tool(name='android-app'). Handles multiple sub-actions via AppAction enum by dispatching to internal helper functions.@mcp.tool(name="android-app") async def app_operations( # pylint: disable=too-many-arguments serial: str, action: AppAction, ctx: Context, package: str | None = None, # Required for most actions apk_path: str | None = None, # For install_app reinstall: bool = False, # For install_app grant_permissions: bool = True, # For install_app keep_data: bool = False, # For uninstall_app activity: str = "", # For start_app extras: dict[str, str] | None = None, # For start_intent include_system_apps: bool = False, # For list_packages include_app_name: bool = False, # For list_packages include_apk_path: bool = True, # For list_packages max_packages: int | None = 200, # For list_packages ) -> str: """ Perform various application management operations on an Android device. This single tool consolidates various app-related actions. The 'action' parameter determines the operation. Args: serial: Device serial number. action: The specific app operation to perform. ctx: MCP Context for logging and interaction. package (Optional[str]): Package name for the target application. Required by most actions. apk_path (Optional[str]): Path to the APK file (local to the server). Used by `install_app`. reinstall (Optional[bool]): Whether to reinstall if app exists. Used by `install_app`. grant_permissions (Optional[bool]): Whether to grant all requested permissions. Used by `install_app`. keep_data (Optional[bool]): Whether to keep app data and cache directories. Used by `uninstall_app`. activity (Optional[str]): Optional activity name to start. Used by `start_app`. extras (Optional[dict[str, str]]): Optional intent extras. Used by `start_intent`. include_system_apps (Optional[bool]): Whether to include system apps. Used by `list_packages`. include_app_name (Optional[bool]): Whether to include app labels (best-effort). Used by `list_packages`. include_apk_path (Optional[bool]): Whether to include APK paths. Used by `list_packages`. max_packages (Optional[int]): Max packages to return. Used by `list_packages`. Returns: A string message indicating the result or status of the operation. --- Available Actions and their specific argument usage: 1. `action="install_app"` - Requires: `apk_path` - Optional: `reinstall`, `grant_permissions` 2. `action="uninstall_app"` - Requires: `package` - Optional: `keep_data` 3. `action="start_app"` - Requires: `package` - Optional: `activity` 3b. `action="start_intent"` - Requires: `package`, `activity` - Optional: `extras` 4. `action="stop_app"` - Requires: `package` 5. `action="clear_app_data"` - Requires: `package` 6. `action="list_packages"` - Optional: `include_system_apps`, `include_app_name`, `include_apk_path`, `max_packages` 7. `action="get_app_manifest"` - Requires: `package` 8. `action="get_app_permissions"` - Requires: `package` 9. `action="get_app_activities"` - Requires: `package` 10. `action="get_app_info"` - Requires: `package` --- """ try: # Basic argument checks if ( action in [ AppAction.UNINSTALL_APP, AppAction.START_APP, AppAction.START_INTENT, AppAction.STOP_APP, AppAction.CLEAR_APP_DATA, AppAction.GET_APP_MANIFEST, AppAction.GET_APP_PERMISSIONS, AppAction.GET_APP_ACTIVITIES, AppAction.GET_APP_INFO, ] and package is None ): return f"❌ Error: 'package' is required for action '{action.value}'." if action == AppAction.INSTALL_APP and apk_path is None: return "❌ Error: 'apk_path' is required for action 'install_app'." if action == AppAction.START_INTENT and not activity: return "❌ Error: 'activity' is required for action 'start_intent'." # Dispatch to implementations if action == AppAction.INSTALL_APP: # We already checked apk_path is not None return await _install_app_impl(serial, apk_path, ctx, reinstall, grant_permissions) # type: ignore if action == AppAction.UNINSTALL_APP: return await _uninstall_app_impl(serial, package, ctx, keep_data) # type: ignore if action == AppAction.START_APP: return await _start_app_impl(serial, package, ctx, activity) # type: ignore if action == AppAction.START_INTENT: assert package is not None return await start_intent( serial=serial, package=package, activity=activity, ctx=ctx, extras=extras, device_manager=get_device_manager(), ) # type: ignore[arg-type] if action == AppAction.STOP_APP: return await _stop_app_impl(serial, package, ctx) # type: ignore if action == AppAction.CLEAR_APP_DATA: return await _clear_app_data_impl(serial, package, ctx) # type: ignore if action == AppAction.LIST_PACKAGES: return await _list_packages_impl( serial, ctx, include_system_apps=include_system_apps, include_app_name=include_app_name, include_apk_path=include_apk_path, max_packages=max_packages, ) if action == AppAction.GET_APP_MANIFEST: return await _get_app_manifest_impl(serial, package, ctx) # type: ignore if action == AppAction.GET_APP_PERMISSIONS: return await _get_app_permissions_impl(serial, package, ctx) # type: ignore if action == AppAction.GET_APP_ACTIVITIES: return await _get_app_activities_impl(serial, package, ctx) # type: ignore if action == AppAction.GET_APP_INFO: return await _get_app_info_impl(serial, package, ctx) # type: ignore # Should not be reached if AppAction enum is comprehensive valid_actions = ", ".join([act.value for act in AppAction]) logger.error("Invalid app action '%s' received. Valid actions are: %s", action, valid_actions) return f"❌ Error: Unknown app action '{action}'. Valid actions are: {valid_actions}." except Exception as e: logger.exception( "Unexpected error during app operation %s on %s for package '%s': %s", action, serial, package, e ) return f"❌ Error: An unexpected error occurred during '{action.value}': {e!s}"
- AppAction Enum: Defines all sub-actions (install_app, uninstall_app, etc.) for the android-app tool, used as the 'action' parameter.class AppAction(str, Enum): """Defines the available sub-actions for the 'android-app' tool.""" INSTALL_APP = "install_app" UNINSTALL_APP = "uninstall_app" START_APP = "start_app" START_INTENT = "start_intent" STOP_APP = "stop_app" CLEAR_APP_DATA = "clear_app_data" LIST_PACKAGES = "list_packages" GET_APP_MANIFEST = "get_app_manifest" GET_APP_PERMISSIONS = "get_app_permissions" GET_APP_ACTIVITIES = "get_app_activities" GET_APP_INFO = "get_app_info"
- PackageInfo dataclass: Type definition for extracted package metadata like version, paths, etc.@dataclass class PackageInfo: """Basic package information.""" version_code: str | None = None version_name: str | None = None min_sdk: str | None = None target_sdk: str | None = None install_path: str = "Unknown" first_install: str = "Unknown" last_update: str | None = None user_id: str = "Unknown" cpu_arch: str | None = None data_dir: str | None = None flags: str | None = None
- AppAnalyzer class: Static methods for parsing dumpsys package output to extract and format manifest info, permissions, components (activities, services, etc.).class AppAnalyzer: """Class for analyzing and extracting app information.""" @staticmethod def extract_package_info(dump_output: str) -> PackageInfo: """Extract basic package information from dumpsys output.""" info = PackageInfo() # Version info if match := re.search(r"versionCode=(\d+)", dump_output): info.version_code = match.group(1) if match := re.search(r"versionName=([^\s]+)", dump_output): info.version_name = match.group(1) if match := re.search(r"minSdk=(\d+)", dump_output): info.min_sdk = match.group(1) if match := re.search(r"targetSdk=(\d+)", dump_output): info.target_sdk = match.group(1) # Paths and IDs if match := re.search(r"codePath=([^\s]+)", dump_output): info.install_path = match.group(1) if match := re.search(r"firstInstallTime=([^\r\n]+)", dump_output): info.first_install = match.group(1) if match := re.search(r"lastUpdateTime=([^\r\n]+)", dump_output): info.last_update = match.group(1) if match := re.search(r"userId=(\d+)", dump_output): info.user_id = match.group(1) # System info if match := re.search(r"primaryCpuAbi=([^\s]+)", dump_output): if match.group(1) != "null": info.cpu_arch = match.group(1) if match := re.search(r"dataDir=([^\s]+)", dump_output): info.data_dir = match.group(1) if match := re.search(r"flags=\[\s([^\]]+)\s\]", dump_output): info.flags = match.group(1) return info @staticmethod def format_package_info(info: PackageInfo) -> str: """Format package information as markdown.""" manifest = "## Package Information\n\n" # Version info if any([info.version_code, info.version_name, info.min_sdk, info.target_sdk]): if info.version_code: manifest += f"- **Version Code**: {info.version_code}\n" if info.version_name: manifest += f"- **Version Name**: {info.version_name}\n" if info.min_sdk: manifest += f"- **Min SDK**: {info.min_sdk}\n" if info.target_sdk: manifest += f"- **Target SDK**: {info.target_sdk}\n" # Paths and dates manifest += f"- **Install Path**: {info.install_path}\n" manifest += f"- **First Install**: {info.first_install}\n" if info.last_update: manifest += f"- **Last Update**: {info.last_update}\n" manifest += f"- **User ID**: {info.user_id}\n" # Optional system info if info.cpu_arch: manifest += f"- **CPU Architecture**: {info.cpu_arch}\n" if info.data_dir: manifest += f"- **Data Directory**: {info.data_dir}\n" if info.flags: manifest += f"- **Flags**: {info.flags}\n" return manifest @staticmethod def extract_permissions(dump_output: str) -> tuple[list[str], list[str]]: """Extract declared and requested permissions from dumpsys output.""" declared_perms = [] requested_perms = [] # Extract declared permissions if declared_match := re.compile(r"declared permissions:\s*\r?\n((?:\s+[^\r\n]+\r?\n)+)", re.MULTILINE).search( dump_output ): declared_block = declared_match.group(1) for line in declared_block.split("\n"): if perm_match := re.match(r"\s+([^:]+):", line.strip()): declared_perms.append(perm_match.group(1)) # Extract requested permissions if requested_match := re.compile(r"requested permissions:\s*\r?\n((?:\s+[^\r\n]+\r?\n)+)", re.MULTILINE).search( dump_output ): requested_block = requested_match.group(1) requested_perms = [line.strip() for line in requested_block.split("\n") if line.strip()] return declared_perms, requested_perms @staticmethod def format_permissions(declared_perms: list[str], requested_perms: list[str]) -> str: """Format permissions as markdown.""" manifest = "\n## Permissions\n\n" # Declared permissions manifest += "### Declared Permissions\n\n" if declared_perms: for perm in declared_perms: manifest += f"- `{perm}`\n" else: manifest += "No declared permissions.\n" # Requested permissions manifest += "\n### Requested Permissions\n\n" if requested_perms: for perm in requested_perms: manifest += f"- `{perm}`\n" else: manifest += "No requested permissions.\n" return manifest @staticmethod def extract_components(dump_output: str, package: str) -> tuple[list[str], list[str], list[str], list[str]]: """Extract activities, services, providers, and receivers from dumpsys output.""" activities = [] services = [] providers = [] receivers = [] # Extract activities activity_pattern = re.compile(r"([a-zA-Z0-9_$.\/]+/[a-zA-Z0-9_$.]+) filter", re.MULTILINE) main_activity_pattern = re.compile(r"([a-zA-Z0-9_$.]+/\.[a-zA-Z0-9_$.]+) filter", re.MULTILINE) for match in activity_pattern.finditer(dump_output): activity = match.group(1) if activity not in activities and activity.startswith(f"{package}/"): activities.append(activity) for match in main_activity_pattern.finditer(dump_output): activity = match.group(1) if activity not in activities and activity.startswith(f"{package}/"): activities.append(activity) # Extract services if service_section_match := re.search( r"Service Resolver Table:(.*?)(?:\r?\n\r?\n|\r?\nProvider Resolver Table:)", dump_output, re.DOTALL ): service_section = service_section_match.group(1) service_pattern = re.compile(r"([a-zA-Z0-9_$.\/]+/[a-zA-Z0-9_$.]+)", re.MULTILINE) for match in service_pattern.finditer(service_section): service = match.group(1) if service not in services and service.startswith(f"{package}/"): services.append(service) # Extract providers if provider_section_match := re.search( r"Provider Resolver Table:(.*?)(?:\r?\n\r?\n|\r?\nReceiver Resolver Table:)", dump_output, re.DOTALL ): provider_section = provider_section_match.group(1) provider_pattern = re.compile(r"([a-zA-Z0-9_$.\/]+/[a-zA-Z0-9_$.]+)", re.MULTILINE) for match in provider_pattern.finditer(provider_section): provider = match.group(1) if provider not in providers and provider.startswith(f"{package}/"): providers.append(provider) # Extract receivers if receiver_section_match := re.search( r"Receiver Resolver Table:(.*?)(?:\r?\n\r?\n|\r?\nService Resolver Table:)", dump_output, re.DOTALL ): receiver_section = receiver_section_match.group(1) receiver_pattern = re.compile(r"([a-zA-Z0-9_$.\/]+/[a-zA-Z0-9_$.]+)", re.MULTILINE) for match in receiver_pattern.finditer(receiver_section): receiver = match.group(1) if receiver not in receivers and receiver.startswith(f"{package}/"): receivers.append(receiver) return activities, services, providers, receivers @staticmethod def get_intent_filters(component: str, dump_output: str) -> list[str]: """Extract intent filters for a component.""" component_short = component.split("/")[-1] filters = [] in_filter = False for line in dump_output.splitlines(): if component_short in line and "filter" in line: in_filter = True continue if in_filter: if not line.strip() or line.startswith(" Filter"): continue if not line.startswith(" " * 10): in_filter = False break filter_info = line.strip() if filter_info and filter_info not in filters: filters.append(filter_info) return filters @staticmethod def format_component_section(title: str, components: list[str], dump_output: str) -> str: """Format a component section as markdown.""" manifest = f"\n### {title}\n\n" if not components: manifest += f"No {title.lower()} found.\n" return manifest for component in components: manifest += f"- `{component}`\n" filters = AppAnalyzer.get_intent_filters(component, dump_output) if filters: manifest += " Intent Filters:\n" for f in filters: manifest += f" - {f}\n" return manifest @staticmethod def format_components( activities: list[str], services: list[str], providers: list[str], receivers: list[str], dump_output: str ) -> str: """Format all components as markdown.""" manifest = "\n## Components\n" manifest += AppAnalyzer.format_component_section("Activities", activities, dump_output) manifest += AppAnalyzer.format_component_section("Services", services, dump_output) manifest += AppAnalyzer.format_component_section("Content Providers", providers, dump_output) manifest += AppAnalyzer.format_component_section("Broadcast Receivers", receivers, dump_output) return manifest
- droidmind/tools/app_management.py:750-750 (registration)Tool registration decorator @mcp.tool(name='android-app') applied to the handler function.@mcp.tool(name="android-app")