Skip to main content
Glama
android.ts30.9 kB
/** * @fileoverview Android Tools for MCP Mobile Server * * This module provides comprehensive Android development tools for the MCP protocol, * enabling AI agents to interact with Android SDK, ADB (Android Debug Bridge), AVD * (Android Virtual Device), emulators, and the complete Android development ecosystem. * * @module tools/android * @category Core Tools * * Key Features: * - Android SDK package management (list, install) * - ADB device interaction (list, install, uninstall, shell, logcat) * - AVD management (create, delete, list) * - Emulator control (start, stop, with advanced options) * - Integration with specialized tools (Gradle, Lint, Media, native-run) * - Automatic fallback system (ADB → native-run) * - Process lifecycle tracking for emulators * * @example * ```typescript * const androidTools = createAndroidTools(globalProcessMap); * const devicesTool = androidTools.get('android_list_devices'); * const result = await devicesTool.handler({}); * console.log(result.data.devices); * ``` */ import { z } from 'zod'; import { processExecutor } from '../utils/process.js'; import { fallbackManager } from '../utils/fallbacks.js'; import path from 'path'; import fs from 'fs/promises'; import { spawn } from 'child_process'; import { createAndroidGradleTools } from './android/gradle.js'; import { createAndroidLintTools } from './android/lint.js'; import { createAndroidMediaTools } from './android/media.js'; import { createNativeRunTools } from './android/native-run.js'; /** * Global map tracking running Android emulator processes. * Maps AVD names to process IDs. * * @type {Map<string, number>} */ let runningEmulators: Map<string, number>; /** * Zod validation schema for android_sdk_install_packages tool. * Validates SDK package installation parameters. * * @type {z.ZodObject} * @property {string[]} packages - Array of SDK package names to install */ const AndroidPackageSchema = z.object({ packages: z.array(z.string().min(1)).min(1), }); /** * Zod validation schema for android_create_avd tool. * Validates Android Virtual Device creation parameters. * * @type {z.ZodObject} * @property {string} name - AVD name * @property {string} systemImageId - System image identifier (e.g., "system-images;android-30;google_apis;x86") * @property {string} [device] - Device profile (optional) * @property {string} [sdcard] - SD card size (e.g., "512M", "1G") */ const AndroidAvdCreateSchema = z.object({ name: z.string().min(1), systemImageId: z.string().min(1), device: z.string().optional(), sdcard: z.string().optional(), }); /** * Zod validation schema for android_start_emulator tool. * Validates Android emulator startup parameters. * * @type {z.ZodObject} * @property {string} avdName - AVD name to start * @property {Object} [options] - Emulator startup options * @property {boolean} [options.noWindow] - Run headless without GUI * @property {number} [options.port] - Emulator port (5554-5682) * @property {string} [options.gpu] - GPU acceleration mode */ const AndroidEmulatorStartSchema = z.object({ avdName: z.string().min(1), options: z.object({ noWindow: z.boolean().optional(), port: z.number().min(5554).max(5682).optional(), gpu: z.enum(['auto', 'host', 'swiftshader_indirect', 'angle_indirect', 'guest']).optional(), }).optional(), }); /** * Zod validation schema for android_install_apk tool. * Validates APK installation parameters. * * @type {z.ZodObject} * @property {string} serial - Device serial number * @property {string} apkPath - Path to APK file * @property {Object} [options] - Installation options * @property {boolean} [options.replace] - Replace existing app * @property {boolean} [options.test] - Install as test APK */ const AndroidAdbInstallSchema = z.object({ serial: z.string().min(1), apkPath: z.string().min(1), options: z.object({ replace: z.boolean().optional(), test: z.boolean().optional(), }).optional(), }); /** * Zod validation schema for android_adb_shell tool. * Validates ADB shell command parameters. * * @type {z.ZodObject} * @property {string} serial - Device serial number * @property {string} command - Shell command to execute */ const AndroidAdbShellSchema = z.object({ serial: z.string().min(1), command: z.string().min(1), }); /** * Zod validation schema for android_stop_emulator tool. * * @type {z.ZodObject} * @property {string} avdName - AVD name to stop */ const AndroidEmulatorStopSchema = z.object({ avdName: z.string().min(1), }); /** * Zod validation schema for android_adb_uninstall tool. * * @type {z.ZodObject} * @property {string} serial - Device serial number * @property {string} packageName - Android package name to uninstall */ const AndroidAdbUninstallSchema = z.object({ serial: z.string().min(1), packageName: z.string().min(1), }); /** * Zod validation schema for android_delete_avd tool. * * @type {z.ZodObject} * @property {string} name - AVD name to delete */ const AndroidAvdDeleteSchema = z.object({ name: z.string().min(1), }); /** * Zod validation schema for android_adb_logcat tool. * Validates logcat command parameters. * * @type {z.ZodObject} * @property {string} serial - Device serial number * @property {string} [filter] - Logcat filter expression * @property {number} lines - Number of lines to retrieve (1-10000, default: 100) * @property {boolean} clear - Clear logcat buffer before reading */ const AndroidAdbLogcatSchema = z.object({ serial: z.string().min(1), filter: z.string().optional(), lines: z.number().min(1).max(10000).default(100), clear: z.boolean().default(false), }); /** * Creates and configures all Android tools for the MCP Mobile Server. * * This factory function initializes and returns a comprehensive set of Android development tools, * providing complete Android SDK, ADB, and emulator integration for AI agents via the MCP protocol. * * **Core Tools Created:** * - `android_sdk_list_packages`: List available and installed SDK packages * - `android_sdk_install_packages`: Install SDK packages * - `android_list_devices`: List connected Android devices and emulators * - `android_create_avd`: Create Android Virtual Device (AVD) * - `android_delete_avd`: Delete Android Virtual Device * - `android_list_avds`: List all available AVDs * - `android_start_emulator`: Start Android emulator * - `android_stop_emulator`: Stop running emulator * - `android_install_apk`: Install APK on device * - `android_adb_uninstall`: Uninstall app from device * - `android_adb_shell`: Execute shell command on device * - `android_adb_logcat`: Read device logs (logcat) * * **Specialized Tools** (via sub-modules): * - Gradle tools: Build, assemble, test Android projects * - Lint tools: Code quality analysis * - Media tools: Screenshot, screen recording * - Native-run tools: Lightweight alternative to ADB * * **Features:** * - Automatic fallback system (ADB → native-run when ADB unavailable) * - Process lifecycle tracking for emulators * - Security validation for all commands and paths * - Comprehensive error handling with detailed messages * - Support for headless emulators (CI/CD environments) * - GPU acceleration options * * @param {Map<string, number>} globalProcessMap - Global map tracking all active processes * @returns {Map<string, any>} Map of tool names to tool configurations * * @example * ```typescript * const globalProcesses = new Map(); * const tools = createAndroidTools(globalProcesses); * * // List devices * const devicesTool = tools.get('android_list_devices'); * const devices = await devicesTool.handler({}); * console.log(devices.data.devices); * * // Create and start emulator * const createAvd = tools.get('android_create_avd'); * await createAvd.handler({ * name: 'test_device', * systemImageId: 'system-images;android-30;google_apis;x86' * }); * * const startEmu = tools.get('android_start_emulator'); * await startEmu.handler({ * avdName: 'test_device', * options: { noWindow: true, gpu: 'swiftshader_indirect' } * }); * ``` * * @throws {Error} If Android SDK is not installed or not configured * @throws {Error} If validation schemas fail for any tool parameters * * @see {@link https://developer.android.com/studio/command-line|Android CLI Reference} * @see {@link https://developer.android.com/studio/command-line/adb|ADB Documentation} */ export function createAndroidTools(globalProcessMap: Map<string, number>): Map<string, any> { // Initialize local emulator tracking if not provided runningEmulators = globalProcessMap; const tools = new Map<string, any>(); // Android SDK Manager - List packages tools.set('android_sdk_list_packages', { name: 'android_sdk_list_packages', description: 'List available and installed Android SDK packages', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => { const result = await processExecutor.execute('sdkmanager', ['--list']); if (result.exitCode !== 0) { throw new Error(`SDK Manager failed: ${result.stderr}`); } // Parse sdkmanager output const packages = []; const lines = result.stdout.split('\n'); let inPackageSection = false; for (const line of lines) { if (line.includes('Available Packages:') || line.includes('Installed packages:')) { inPackageSection = true; continue; } if (inPackageSection && line.trim() && !line.startsWith('-') && !line.startsWith('Path') && !line.startsWith('Description')) { const parts = line.trim().split(/\s+/); if (parts.length > 0 && parts[0] && !line.includes('Version') && !line.includes('Location')) { packages.push(parts[0]); } } } return { success: true, data: { packages: packages.filter(pkg => pkg.length > 0), rawOutput: result.stdout, }, }; } }); // Android SDK Manager - Install packages tools.set('android_sdk_install_packages', { name: 'android_sdk_install_packages', description: 'Install Android SDK packages', inputSchema: { type: 'object', properties: { packages: { type: 'array', items: { type: 'string', minLength: 1 }, minItems: 1, description: 'Array of SDK package names to install' } }, required: ['packages'] }, handler: async (args: any) => { const validation = AndroidPackageSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { packages } = validation.data; // Validate package names (only alphanumeric, dots, dashes, colons, semicolons) const validPackagePattern = /^[a-zA-Z0-9.:;-]+$/; for (const pkg of packages) { if (!validPackagePattern.test(pkg)) { throw new Error(`Invalid package name: ${pkg}. Package names can only contain alphanumeric characters, dots, colons, semicolons, and dashes`); } } const result = await processExecutor.execute( 'sdkmanager', [...packages], { timeout: 600000 } // 10 minutes timeout for SDK installations ); if (result.exitCode !== 0) { throw new Error(`SDK package installation failed: ${result.stderr}`); } return { success: true, data: { installedPackages: packages, output: result.stdout, }, }; } }); // Android AVD Manager - List AVDs tools.set('android_list_avds', { name: 'android_list_avds', description: 'List Android Virtual Devices (AVDs)', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => { const result = await processExecutor.execute('avdmanager', ['list', 'avd']); if (result.exitCode !== 0) { throw new Error(`AVD Manager failed: ${result.stderr}`); } // Parse AVD list output const avds = []; const lines = result.stdout.split('\n'); let currentAvd: any = {}; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('Name:')) { if (currentAvd.name) { avds.push(currentAvd); } currentAvd = { name: trimmed.replace('Name:', '').trim() }; } else if (trimmed.startsWith('Path:')) { currentAvd.path = trimmed.replace('Path:', '').trim(); } else if (trimmed.startsWith('Target:')) { currentAvd.target = trimmed.replace('Target:', '').trim(); } else if (trimmed.startsWith('Based on:')) { currentAvd.systemImage = trimmed.replace('Based on:', '').trim(); } } // Add last AVD if exists if (currentAvd.name) { avds.push(currentAvd); } return { success: true, data: { avds, rawOutput: result.stdout, }, }; } }); // Android AVD Manager - Create AVD tools.set('android_create_avd', { name: 'android_create_avd', description: 'Create a new Android Virtual Device (AVD)', inputSchema: { type: 'object', properties: { name: { type: 'string', minLength: 1, description: 'AVD name (alphanumeric and underscores only)' }, systemImageId: { type: 'string', minLength: 1, description: 'System image package ID' }, device: { type: 'string', description: 'Device definition (optional)' }, sdcard: { type: 'string', description: 'SD card size (e.g., 512M, 1G)' } }, required: ['name', 'systemImageId'] }, handler: async (args: any) => { const validation = AndroidAvdCreateSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { name, systemImageId, sdcard, device } = validation.data; // Validate AVD name (only alphanumeric and underscores) if (!/^[a-zA-Z0-9_]+$/.test(name)) { throw new Error('AVD name can only contain alphanumeric characters and underscores'); } const args_cmd = ['create', 'avd', '--name', name, '--package', systemImageId]; if (device) { args_cmd.push('--device', device); } if (sdcard) { args_cmd.push('--sdcard', sdcard); } const result = await processExecutor.execute('avdmanager', args_cmd, { timeout: 300000, // 5 minutes timeout }); if (result.exitCode !== 0) { throw new Error(`AVD creation failed: ${result.stderr}`); } return { success: true, data: { avdName: name, systemImage: systemImageId, output: result.stdout, }, }; } }); // Android AVD Manager - Delete AVD tools.set('android_delete_avd', { name: 'android_delete_avd', description: 'Delete an Android Virtual Device (AVD)', inputSchema: { type: 'object', properties: { name: { type: 'string', minLength: 1, description: 'AVD name to delete' } }, required: ['name'] }, handler: async (args: any) => { const validation = AndroidAvdDeleteSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { name } = validation.data; // Validate AVD name if (!/^[a-zA-Z0-9_]+$/.test(name)) { throw new Error('Invalid AVD name. Name can only contain alphanumeric characters and underscores'); } const result = await processExecutor.execute('avdmanager', ['delete', 'avd', '--name', name]); if (result.exitCode !== 0) { throw new Error(`AVD deletion failed: ${result.stderr}`); } // Remove from running emulators tracking if exists runningEmulators.delete(name); return { success: true, data: { deletedAvd: name, output: result.stdout, }, }; } }); // Android Emulator - List emulators tools.set('android_list_emulators', { name: 'android_list_emulators', description: 'List available Android emulators', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => { const result = await processExecutor.execute('emulator', ['-list-avds']); if (result.exitCode !== 0) { throw new Error(`Emulator listing failed: ${result.stderr}`); } const emulators = result.stdout .split('\n') .map(line => line.trim()) .filter(line => line.length > 0) .map(name => ({ name, isRunning: runningEmulators.has(name), pid: runningEmulators.get(name) || null, })); return { success: true, data: { emulators, totalCount: emulators.length, runningCount: emulators.filter(e => e.isRunning).length, }, }; } }); // Android Emulator - Start emulator tools.set('android_start_emulator', { name: 'android_start_emulator', description: 'Start an Android emulator', inputSchema: { type: 'object', properties: { avdName: { type: 'string', minLength: 1, description: 'AVD name to start' }, options: { type: 'object', properties: { noWindow: { type: 'boolean', description: 'Run without UI window' }, port: { type: 'number', minimum: 5554, maximum: 5682, description: 'Console port number' }, gpu: { type: 'string', enum: ['auto', 'host', 'swiftshader_indirect', 'angle_indirect', 'guest'], description: 'GPU acceleration mode' } } } }, required: ['avdName'] }, handler: async (args: any) => { const validation = AndroidEmulatorStartSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { avdName, options = {} } = validation.data; // Check if emulator is already running if (runningEmulators.has(avdName)) { return { success: true, data: { avdName, status: 'already_running', pid: runningEmulators.get(avdName), message: 'Emulator is already running', }, }; } const emulator_args = ['-avd', avdName]; if (options.noWindow) { emulator_args.push('-no-window'); } if (options.port) { emulator_args.push('-port', options.port.toString()); } if (options.gpu) { emulator_args.push('-gpu', options.gpu); } // Start emulator in background const emulatorProcess = spawn('emulator', emulator_args, { detached: true, stdio: 'ignore', }); // Track the process runningEmulators.set(avdName, emulatorProcess.pid!); // Handle process exit emulatorProcess.on('exit', () => { runningEmulators.delete(avdName); }); // Unref to allow the parent process to exit emulatorProcess.unref(); return { success: true, data: { avdName, pid: emulatorProcess.pid, status: 'started', options, }, }; } }); // Android Emulator - Stop emulator tools.set('android_stop_emulator', { name: 'android_stop_emulator', description: 'Stop a running Android emulator', inputSchema: { type: 'object', properties: { avdName: { type: 'string', minLength: 1, description: 'AVD name to stop' } }, required: ['avdName'] }, handler: async (args: any) => { const validation = AndroidEmulatorStopSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { avdName } = validation.data; const pid = runningEmulators.get(avdName); if (!pid) { throw new Error(`Emulator is not running or not tracked by this server. No running emulator found for AVD: ${avdName}`); } try { // Kill the process process.kill(pid, 'SIGTERM'); // Remove from tracking runningEmulators.delete(avdName); return { success: true, data: { avdName, pid, status: 'stopped', }, }; } catch (killError) { // Process might already be dead runningEmulators.delete(avdName); return { success: true, data: { avdName, pid, status: 'already_stopped', message: 'Process was already terminated', }, }; } } }); // Android ADB - List devices tools.set('android_list_devices', { name: 'android_list_devices', description: 'List connected Android devices and emulators (supports ADB fallback to native-run)', inputSchema: { type: 'object', properties: {}, required: [] }, handler: async () => { const fallbackResult = await fallbackManager.executeAdbWithFallback( ['devices', '-l'], { platform: 'android' } ); if (!fallbackResult.success) { throw new Error(fallbackResult.error || 'Failed to list devices'); } const result = fallbackResult.data; // Parse adb devices output const devices = []; const lines = result.stdout.split('\n').slice(1); // Skip header for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('*')) { const parts = trimmed.split(/\s+/); if (parts.length >= 2) { const device = { serial: parts[0], state: parts[1], product: '', model: '', device: '', transport_id: '', }; // Parse additional info for (let i = 2; i < parts.length; i++) { const part = parts[i]; if (part && part.startsWith('product:')) { device.product = part.split(':')[1] || ''; } else if (part && part.startsWith('model:')) { device.model = part.split(':')[1] || ''; } else if (part && part.startsWith('device:')) { device.device = part.split(':')[1] || ''; } else if (part && part.startsWith('transport_id:')) { device.transport_id = part.split(':')[1] || ''; } } devices.push(device); } } } return { success: true, data: { devices, totalCount: devices.length, onlineCount: devices.filter(d => d.state === 'device').length, fallbackInfo: fallbackResult.usedFallback ? { usedFallback: true, fallbackTool: fallbackResult.fallbackTool, message: fallbackResult.message } : undefined }, }; } }); // Android ADB - Install APK tools.set('android_install_apk', { name: 'android_install_apk', description: 'Install an APK file to an Android device or emulator', inputSchema: { type: 'object', properties: { serial: { type: 'string', minLength: 1, description: 'Device serial number' }, apkPath: { type: 'string', minLength: 1, description: 'Path to APK file' }, options: { type: 'object', properties: { replace: { type: 'boolean', description: 'Replace existing app if installed' }, test: { type: 'boolean', description: 'Allow test APKs' } } } }, required: ['serial', 'apkPath'] }, handler: async (args: any) => { const validation = AndroidAdbInstallSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { serial, apkPath, options = {} } = validation.data; // Validate APK path if (!apkPath.endsWith('.apk')) { throw new Error(`File must have .apk extension. Invalid path: ${apkPath}`); } // Check if APK file exists try { await fs.access(apkPath); } catch { throw new Error(`APK file not found: ${apkPath}`); } const adb_args = ['-s', serial, 'install']; if (options.replace) { adb_args.push('-r'); } if (options.test) { adb_args.push('-t'); } adb_args.push(apkPath); const result = await processExecutor.execute('adb', adb_args, { timeout: 300000, // 5 minutes timeout for APK installation }); if (result.exitCode !== 0) { throw new Error(`APK installation failed: ${result.stderr || result.stdout}`); } return { success: true, data: { serial, apkPath: path.basename(apkPath), output: result.stdout, }, }; } }); // Android ADB - Uninstall package tools.set('android_uninstall_package', { name: 'android_uninstall_package', description: 'Uninstall an Android application package', inputSchema: { type: 'object', properties: { serial: { type: 'string', minLength: 1, description: 'Device serial number' }, packageName: { type: 'string', minLength: 1, description: 'Android package name (e.g., com.example.app)' } }, required: ['serial', 'packageName'] }, handler: async (args: any) => { const validation = AndroidAdbUninstallSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { serial, packageName } = validation.data; // Validate package name format if (!/^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*$/.test(packageName)) { throw new Error(`Invalid Android package name format: ${packageName}. Package name must follow Android naming conventions`); } const result = await processExecutor.execute('adb', ['-s', serial, 'uninstall', packageName]); if (result.exitCode !== 0) { throw new Error(`Package uninstallation failed: ${result.stderr || result.stdout}`); } return { success: true, data: { serial, packageName, output: result.stdout, }, }; } }); // Android ADB - Execute shell command tools.set('android_shell_command', { name: 'android_shell_command', description: 'Execute a shell command on an Android device or emulator', inputSchema: { type: 'object', properties: { serial: { type: 'string', minLength: 1, description: 'Device serial number' }, command: { type: 'string', minLength: 1, description: 'Shell command to execute' } }, required: ['serial', 'command'] }, handler: async (args: any) => { const validation = AndroidAdbShellSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { serial, command } = validation.data; // Additional validation for dangerous commands const dangerousCommands = ['rm -rf', 'format', 'dd if=', 'dd of=', '>/dev/', 'chmod 777']; for (const dangerous of dangerousCommands) { if (command.includes(dangerous)) { throw new Error(`Command contains dangerous operations. Blocked command pattern: ${dangerous}`); } } const result = await processExecutor.execute('adb', ['-s', serial, 'shell', command]); return { success: true, data: { serial, command, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, duration: result.duration, }, }; } }); // Android ADB - Logcat tools.set('android_logcat', { name: 'android_logcat', description: 'Capture Android logcat output from device or emulator', inputSchema: { type: 'object', properties: { serial: { type: 'string', minLength: 1, description: 'Device serial number' }, filter: { type: 'string', description: 'Log filter (e.g., *:E for errors only)' }, lines: { type: 'number', minimum: 1, maximum: 10000, description: 'Number of log lines to capture' }, clear: { type: 'boolean', description: 'Clear logcat buffer before capturing' } }, required: ['serial'] }, handler: async (args: any) => { const validation = AndroidAdbLogcatSchema.safeParse(args); if (!validation.success) { throw new Error(`Invalid request: ${validation.error.message}`); } const { serial, filter, lines = 100, clear = false } = validation.data; const adb_args = ['-s', serial, 'logcat']; if (clear) { adb_args.push('-c'); const clearResult = await processExecutor.execute('adb', adb_args); return { success: true, data: { serial, action: 'cleared', output: 'Logcat cleared successfully', }, }; } // Add filter if provided if (filter) { adb_args.push(filter); } // Add line limit adb_args.push('-t', lines.toString()); const result = await processExecutor.execute('adb', adb_args, { timeout: 60000, // 1 minute timeout for logcat }); return { success: true, data: { serial, filter: filter || 'all', lines, logs: result.stdout, exitCode: result.exitCode, }, }; } }); // Add new Android tools const gradleTools = createAndroidGradleTools(); const lintTools = createAndroidLintTools(); const mediaTools = createAndroidMediaTools(); const nativeRunTools = createNativeRunTools(); // Merge all tools for (const [name, tool] of gradleTools.entries()) { tools.set(name, tool); } for (const [name, tool] of lintTools.entries()) { tools.set(name, tool); } for (const [name, tool] of mediaTools.entries()) { tools.set(name, tool); } for (const [name, tool] of nativeRunTools.entries()) { tools.set(name, tool); } return tools; }

Implementation Reference

Latest Blog Posts

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/cristianoaredes/mcp-mobile-server'

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