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:
commit
db6998510c
68
.gitignore
vendored
Normal file
68
.gitignore
vendored
Normal 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
28
Dockerfile
Normal 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
133
README.md
Normal 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
19
docker-compose.yml
Normal 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
52
pyproject.toml
Normal 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
1
src/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Android MCP Server
|
218
src/server.py
Normal file
218
src/server.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user