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