Initial commit: Android ADB MCP Server

- FastMCP server with STDIO interface
- Comprehensive ADB tools for device automation
- Docker support with USB device access
- Console script entry point for uvx compatibility
- Type-safe Pydantic models for all parameters

Tools included:
- adb_devices: List connected Android devices
- adb_screenshot: Capture and retrieve screenshots
- adb_input: Send taps, swipes, key events, text
- adb_launch_app: Launch apps by package name
- adb_launch_url: Open URLs in browser
- adb_list_packages: List installed packages
- adb_shell_command: Execute shell commands

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ryan Malloy 2025-08-12 11:56:14 -06:00
commit db6998510c
8 changed files with 1815 additions and 0 deletions

68
.gitignore vendored Normal file
View File

@ -0,0 +1,68 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Testing
.coverage
.pytest_cache/
htmlcov/
.tox/
# Linting
.ruff_cache/
.mypy_cache/
# Screenshots
*.png
*.jpg
*.jpeg
# Docker
.dockerignore
# uv
.uv/

28
Dockerfile Normal file
View File

@ -0,0 +1,28 @@
FROM python:3.11-slim
# Install system dependencies including ADB
RUN apt-get update && apt-get install -y \
android-tools-adb \
android-tools-fastboot \
usbutils \
&& rm -rf /var/lib/apt/lists/*
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
# Set working directory
WORKDIR /app
# Copy project files
COPY pyproject.toml uv.lock* ./
# Install dependencies
RUN uv sync --frozen
# Copy source code
COPY src/ ./src/
# Expose ADB server port (optional, mainly for debugging)
EXPOSE 5037
CMD ["uv", "run", "python", "-m", "src.server"]

133
README.md Normal file
View File

@ -0,0 +1,133 @@
# Android MCP Server
A Model Context Protocol (MCP) server for Android device automation via ADB. This server provides tools for interacting with Android devices through ADB commands in a structured, type-safe way.
## Features
- **Device Management**: List and interact with connected Android devices
- **Screenshots**: Capture and retrieve device screenshots
- **Input Simulation**: Send taps, swipes, key events, and text input
- **App Control**: Launch apps by package name or open URLs
- **Package Management**: List installed packages
- **Shell Commands**: Execute arbitrary shell commands on device
## Tools Available
### `adb_devices()`
List all connected Android devices with their IDs and status.
### `adb_screenshot(device_id?, local_filename?)`
Take a screenshot and save it locally.
### `adb_input(action, device_id?)`
Send input events:
- `tap`: Tap at coordinates (x, y)
- `swipe`: Swipe from (x, y) to (x2, y2)
- `key`: Send key event (key_code)
- `text`: Type text
### `adb_launch_app(package_name, device_id?)`
Launch an app by package name.
### `adb_launch_url(url, device_id?)`
Open URL in default browser.
### `adb_list_packages(device_id?, filter_text?)`
List installed packages, optionally filtered.
### `adb_shell_command(command, device_id?)`
Execute shell command on device.
## Usage
### Using uvx (Recommended)
```bash
# Run directly with uvx
uvx android-mcp-server
# Or from PyPI once published
uvx android-mcp-server
```
### Local Development
```bash
# Install dependencies
uv sync
# Run the server
uv run android-mcp-server
```
### Docker Development
```bash
# Build and run with Docker Compose
docker-compose up --build
# Or build manually
docker build -t android-mcp-server .
docker run --privileged -v /dev/bus/usb:/dev/bus/usb android-mcp-server
```
## MCP Client Configuration
### Using uvx (Recommended)
Add to your MCP client configuration:
```json
{
"mcpServers": {
"android-adb": {
"command": "uvx",
"args": ["android-mcp-server"]
}
}
}
```
### Local Development
```json
{
"mcpServers": {
"android-adb": {
"command": "uv",
"args": ["run", "android-mcp-server"],
"cwd": "/path/to/android-mcp-server"
}
}
}
```
### Docker
```json
{
"mcpServers": {
"android-adb": {
"command": "docker",
"args": ["run", "--privileged", "-v", "/dev/bus/usb:/dev/bus/usb", "android-mcp-server"]
}
}
}
```
## Requirements
- Python 3.11+
- ADB (Android Debug Bridge)
- USB access to Android devices
- Device with USB debugging enabled
## Development
```bash
# Install dev dependencies
uv sync --group dev
# Format code
uv run black src/
# Lint
uv run ruff check src/
# Type check
uv run mypy src/
```

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
version: '3.8'
services:
android-mcp-server:
build: .
volumes:
- /dev/bus/usb:/dev/bus/usb
- ./src:/app/src
- ./pyproject.toml:/app/pyproject.toml
- ./uv.lock:/app/uv.lock
privileged: true
stdin_open: true
tty: true
environment:
- PYTHONPATH=/app
working_dir: /app
command: ["python", "-m", "src.server"]
devices:
- /dev/bus/usb

52
pyproject.toml Normal file
View File

@ -0,0 +1,52 @@
[project]
name = "android-mcp-server"
version = "0.1.0"
description = "Android ADB MCP Server for device automation"
authors = [
{name = "Ryan", email = "ryan@example.com"}
]
dependencies = [
"fastmcp>=0.2.0",
"pydantic>=2.0.0",
]
requires-python = ">=3.11"
[project.scripts]
android-mcp-server = "src.server:main"
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.5.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
dev-dependencies = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.5.0",
]
[tool.black]
line-length = 88
target-version = ['py311']
[tool.ruff]
target-version = "py311"
line-length = 88
[tool.mypy]
python_version = "3.11"
strict = true
[tool.hatch.build.targets.wheel]
packages = ["src"]

1
src/__init__.py Normal file
View File

@ -0,0 +1 @@
# Android MCP Server

218
src/server.py Normal file
View File

@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
Android ADB MCP Server
A Model Context Protocol (MCP) server for Android device automation via ADB.
Provides tools for device interaction, screenshots, app launching, and more.
"""
import asyncio
import json
import subprocess
import sys
from pathlib import Path
from typing import List, Optional, Dict, Any
from fastmcp import FastMCP
from pydantic import BaseModel, Field
class DeviceInfo(BaseModel):
"""Android device information"""
device_id: str
status: str
class ScreenshotResult(BaseModel):
"""Screenshot capture result"""
success: bool
local_path: Optional[str] = None
error: Optional[str] = None
class ADBCommand(BaseModel):
"""ADB command execution parameters"""
command: List[str]
device_id: Optional[str] = None
class InputAction(BaseModel):
"""Input action parameters"""
action_type: str = Field(description="Type of input: tap, swipe, key")
x: Optional[int] = None
y: Optional[int] = None
x2: Optional[int] = None
y2: Optional[int] = None
key_code: Optional[str] = None
text: Optional[str] = None
# Initialize FastMCP server
mcp = FastMCP("android-mcp-server")
async def run_adb_command(cmd: List[str], device_id: Optional[str] = None) -> Dict[str, Any]:
"""Execute ADB command and return result"""
full_cmd = ["adb"]
if device_id:
full_cmd.extend(["-s", device_id])
full_cmd.extend(cmd)
try:
result = await asyncio.create_subprocess_exec(
*full_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await result.communicate()
return {
"success": result.returncode == 0,
"stdout": stdout.decode('utf-8', errors='ignore').strip(),
"stderr": stderr.decode('utf-8', errors='ignore').strip(),
"returncode": result.returncode
}
except Exception as e:
return {
"success": False,
"stdout": "",
"stderr": str(e),
"returncode": -1
}
@mcp.tool()
async def adb_devices() -> List[DeviceInfo]:
"""List all connected Android devices"""
result = await run_adb_command(["devices"])
if not result["success"]:
raise Exception(f"Failed to list devices: {result['stderr']}")
devices = []
lines = result["stdout"].split('\n')[1:] # Skip header
for line in lines:
if line.strip():
parts = line.split('\t')
if len(parts) >= 2:
devices.append(DeviceInfo(
device_id=parts[0],
status=parts[1]
))
return devices
@mcp.tool()
async def adb_screenshot(device_id: Optional[str] = None, local_filename: str = "screenshot.png") -> ScreenshotResult:
"""Take screenshot from Android device and save locally"""
# Take screenshot on device
result = await run_adb_command(["shell", "screencap", "-p", "/sdcard/temp_screenshot.png"], device_id)
if not result["success"]:
return ScreenshotResult(success=False, error=f"Failed to capture screenshot: {result['stderr']}")
# Pull screenshot to local machine
local_path = Path(local_filename).absolute()
pull_result = await run_adb_command(["pull", "/sdcard/temp_screenshot.png", str(local_path)], device_id)
if not pull_result["success"]:
return ScreenshotResult(success=False, error=f"Failed to pull screenshot: {pull_result['stderr']}")
# Clean up device file
await run_adb_command(["shell", "rm", "/sdcard/temp_screenshot.png"], device_id)
return ScreenshotResult(success=True, local_path=str(local_path))
@mcp.tool()
async def adb_input(action: InputAction, device_id: Optional[str] = None) -> Dict[str, Any]:
"""Send input events to Android device"""
if action.action_type == "tap":
if action.x is None or action.y is None:
raise ValueError("tap action requires x and y coordinates")
cmd = ["shell", "input", "tap", str(action.x), str(action.y)]
elif action.action_type == "swipe":
if any(coord is None for coord in [action.x, action.y, action.x2, action.y2]):
raise ValueError("swipe action requires x, y, x2, y2 coordinates")
cmd = ["shell", "input", "swipe", str(action.x), str(action.y), str(action.x2), str(action.y2)]
elif action.action_type == "key":
if action.key_code is None:
raise ValueError("key action requires key_code")
cmd = ["shell", "input", "keyevent", action.key_code]
elif action.action_type == "text":
if action.text is None:
raise ValueError("text action requires text")
cmd = ["shell", "input", "text", action.text]
else:
raise ValueError(f"Unknown action type: {action.action_type}")
result = await run_adb_command(cmd, device_id)
return result
@mcp.tool()
async def adb_launch_app(package_name: str, device_id: Optional[str] = None) -> Dict[str, Any]:
"""Launch Android app by package name"""
cmd = ["shell", "monkey", "-p", package_name, "-c", "android.intent.category.LAUNCHER", "1"]
result = await run_adb_command(cmd, device_id)
return result
@mcp.tool()
async def adb_launch_url(url: str, device_id: Optional[str] = None) -> Dict[str, Any]:
"""Open URL in default browser"""
cmd = ["shell", "am", "start", "-a", "android.intent.action.VIEW", "-d", url]
result = await run_adb_command(cmd, device_id)
return result
@mcp.tool()
async def adb_list_packages(device_id: Optional[str] = None, filter_text: Optional[str] = None) -> Dict[str, Any]:
"""List installed packages on device"""
cmd = ["shell", "pm", "list", "packages"]
if filter_text:
cmd.extend(["|", "grep", filter_text])
result = await run_adb_command(cmd, device_id)
if result["success"]:
packages = []
for line in result["stdout"].split('\n'):
if line.startswith('package:'):
packages.append(line.replace('package:', ''))
return {
"success": True,
"packages": packages,
"count": len(packages)
}
return result
@mcp.tool()
async def adb_shell_command(command: str, device_id: Optional[str] = None) -> Dict[str, Any]:
"""Execute shell command on Android device"""
cmd = ["shell"] + command.split()
result = await run_adb_command(cmd, device_id)
return result
def main():
"""Main entry point for STDIO MCP server - used by console script"""
mcp.run()
if __name__ == "__main__":
main()

1296
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff