Skip to main content
Glama
index.ts19.2 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { ADBWrapper } from './adb-wrapper.js'; import { screenshotHandler, touchHandler, swipeHandler, launchAppHandler, listPackagesHandler, uiautomatorDumpHandler, uiautomatorFindHandler, uiautomatorClickHandler, uiautomatorWaitHandler, uiautomatorSetTextHandler, uiautomatorClearTextHandler, uiautomatorLongClickHandler, uiautomatorDoubleClickHandler, uiautomatorToggleCheckboxHandler, uiautomatorScrollInElementHandler, handleStartScrcpyStream, handleStopScrcpyStream, handleGetLatestFrame, handleCaptureFrameScrcpy, handleSendKeyEvent, handleInputText, handleExecuteCommand } from './handlers.js'; const SERVER_NAME = 'android-mcp-server'; const SERVER_VERSION = '0.1.0'; class AndroidMCPServer { private server: Server; private adb: ADBWrapper; constructor() { this.adb = new ADBWrapper(); this.server = new Server( { name: SERVER_NAME, version: SERVER_VERSION, }, { capabilities: { tools: {}, }, } ); this.setupHandlers(); this.setupErrorHandling(); } private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'android_screenshot', description: 'Capture a screenshot from the Android device', inputSchema: { type: 'object', properties: { outputPath: { type: 'string', description: 'Local path to save the screenshot (optional). If not provided, returns base64 encoded image.', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, }, }, { name: 'android_touch', description: 'Simulate a touch event at specific screen coordinates', inputSchema: { type: 'object', properties: { x: { type: 'number', description: 'X coordinate', }, y: { type: 'number', description: 'Y coordinate', }, duration: { type: 'number', description: 'Touch duration in milliseconds (default: 100)', default: 100, }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['x', 'y'], }, }, { name: 'android_swipe', description: 'Perform a swipe gesture between two coordinates', inputSchema: { type: 'object', properties: { startX: { type: 'number', description: 'Starting X coordinate', }, startY: { type: 'number', description: 'Starting Y coordinate', }, endX: { type: 'number', description: 'Ending X coordinate', }, endY: { type: 'number', description: 'Ending Y coordinate', }, duration: { type: 'number', description: 'Swipe duration in milliseconds (default: 300)', default: 300, }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['startX', 'startY', 'endX', 'endY'], }, }, { name: 'android_launch_app', description: 'Launch an Android app by package name', inputSchema: { type: 'object', properties: { packageName: { type: 'string', description: 'Package name of the app to launch (e.g., com.example.app)', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['packageName'], }, }, { name: 'android_list_packages', description: 'List installed packages on the Android device', inputSchema: { type: 'object', properties: { filter: { type: 'string', description: 'Optional filter to search for specific packages (case-insensitive)', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, }, }, { name: 'android_uiautomator_dump', description: 'Dump the UI hierarchy using UIAutomator and return as XML', inputSchema: { type: 'object', properties: { deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, }, }, { name: 'android_uiautomator_find', description: 'Find UI elements by resource ID or text using UIAutomator', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Resource ID to search for (e.g., com.example.app:id/button_submit)', }, text: { type: 'string', description: 'Text content to search for', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, }, }, { name: 'android_uiautomator_click', description: 'Click on a UI element by resource ID using UIAutomator', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Resource ID of the element to click (e.g., com.example.app:id/button_submit)', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['resourceId'], }, }, { name: 'android_uiautomator_wait', description: 'Wait for a UI element to appear by resource ID', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Resource ID of the element to wait for', }, timeoutMs: { type: 'number', description: 'Maximum time to wait in milliseconds (default: 5000)', default: 5000, }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['resourceId'], }, }, { name: 'android_uiautomator_set_text', description: 'Set text on a UI element by resource ID using UIAutomator', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Resource ID of the element (e.g., com.example.app:id/text_input)', }, text: { type: 'string', description: 'Text to set in the element', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['resourceId', 'text'], }, }, { name: 'android_uiautomator_clear_text', description: 'Clear text from a UI element by resource ID', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Resource ID of the element to clear', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['resourceId'], }, }, { name: 'android_uiautomator_long_click', description: 'Perform a long click on a UI element by resource ID', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Resource ID of the element to long click', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['resourceId'], }, }, { name: 'android_uiautomator_double_click', description: 'Perform a double click on a UI element by resource ID', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Resource ID of the element to double click', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['resourceId'], }, }, { name: 'android_uiautomator_toggle_checkbox', description: 'Toggle a checkbox element by resource ID', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Resource ID of the checkbox element', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['resourceId'], }, }, { name: 'android_uiautomator_scroll_in_element', description: 'Scroll within a specific UI element', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Resource ID of the scrollable element', }, direction: { type: 'string', enum: ['up', 'down', 'left', 'right'], description: 'Direction to scroll', }, distance: { type: 'number', description: 'Distance to scroll in pixels (default: 500)', default: 500, }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['resourceId', 'direction'], }, }, { name: 'android_start_scrcpy_stream', description: 'Start scrcpy streaming for continuous fast frame capture (requires scrcpy installed)', inputSchema: { type: 'object', properties: { deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, }, }, { name: 'android_stop_scrcpy_stream', description: 'Stop scrcpy streaming', inputSchema: { type: 'object', properties: {}, }, }, { name: 'android_get_latest_frame', description: 'Get the latest frame from scrcpy stream (instant access, no latency)', inputSchema: { type: 'object', properties: {}, }, }, { name: 'android_capture_frame_scrcpy', description: 'Capture a single frame via scrcpy (faster than ADB screencap)', inputSchema: { type: 'object', properties: { outputPath: { type: 'string', description: 'Local path to save the frame (optional). If not provided, returns base64 encoded image.', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, }, }, { name: 'android_send_key_event', description: 'Send a key event to the Android device (e.g., KEYEVENT_HOME, KEYEVENT_BACK, KEYEVENT_ENTER)', inputSchema: { type: 'object', properties: { keyCode: { type: 'string', description: 'Key event code (e.g., KEYEVENT_HOME, KEYEVENT_BACK, KEYEVENT_ENTER, 3 for HOME, 4 for BACK). Can be key name or numeric code.', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['keyCode'], }, }, { name: 'android_input_text', description: 'Input text into the currently focused field on the Android device via ADB', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'Text to input. Spaces will be automatically handled.', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['text'], }, }, { name: 'android_execute_command', description: 'Execute a generic ADB command with custom arguments. Allows agents to run any ADB command with their own parameters.', inputSchema: { type: 'object', properties: { args: { type: 'array', items: { type: 'string', }, description: 'Array of ADB command arguments (e.g., ["shell", "pm", "list", "packages"] or ["logcat", "-d", "-s", "MyTag"])', }, deviceSerial: { type: 'string', description: 'Specific device serial number to target (optional)', }, }, required: ['args'], }, }, ], })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'android_screenshot': return await screenshotHandler(this.adb, args); case 'android_touch': return await touchHandler(this.adb, args); case 'android_swipe': return await swipeHandler(this.adb, args); case 'android_launch_app': return await launchAppHandler(this.adb, args); case 'android_list_packages': return await listPackagesHandler(this.adb, args); case 'android_uiautomator_dump': return await uiautomatorDumpHandler(this.adb, args); case 'android_uiautomator_find': return await uiautomatorFindHandler(this.adb, args); case 'android_uiautomator_click': return await uiautomatorClickHandler(this.adb, args); case 'android_uiautomator_wait': return await uiautomatorWaitHandler(this.adb, args); case 'android_uiautomator_set_text': return await uiautomatorSetTextHandler(this.adb, args); case 'android_uiautomator_clear_text': return await uiautomatorClearTextHandler(this.adb, args); case 'android_uiautomator_long_click': return await uiautomatorLongClickHandler(this.adb, args); case 'android_uiautomator_double_click': return await uiautomatorDoubleClickHandler(this.adb, args); case 'android_uiautomator_toggle_checkbox': return await uiautomatorToggleCheckboxHandler(this.adb, args); case 'android_uiautomator_scroll_in_element': return await uiautomatorScrollInElementHandler(this.adb, args); case 'android_start_scrcpy_stream': return await handleStartScrcpyStream(this.adb, args as any); case 'android_stop_scrcpy_stream': return await handleStopScrcpyStream(this.adb, args as any); case 'android_get_latest_frame': return await handleGetLatestFrame(this.adb, args as any); case 'android_capture_frame_scrcpy': return await handleCaptureFrameScrcpy(this.adb, args as any); case 'android_send_key_event': return await handleSendKeyEvent(this.adb, args as any); case 'android_input_text': return await handleInputText(this.adb, args as any); case 'android_execute_command': return await handleExecuteCommand(this.adb, args as any); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Error: ${errorMessage}`, }, ], isError: true, }; } }); } private setupErrorHandling(): void { this.server.onerror = (error) => { console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Android MCP Server running on stdio'); } } const server = new AndroidMCPServer(); server.run().catch(console.error);

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/jduartedj/android-mcp-server'

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