app-automating
An MCP server that drives Android emulators and real devices — and remembers how it got there.
It gives a model three things: control of the Android SDK (create, boot, stop and delete emulators), control of the screen through Appium (tap, type, swipe, scroll, screenshot, read the view hierarchy), and a navigation memory that records every route taken so the next run can look up how it reached a screen instead of rediscovering it.
What you get
28 tools across three modules. Every tool name is prefixed with its module, so a flat tool list stays grouped.
| Module | Tools | What it does |
|---|---|---|
android_ |
10 | Lists devices, creates and renames AVDs, downloads system images, boots emulators headed or headless, stops and deletes them |
appium_ |
13 | Starts and ends sessions, taps by coordinate or locator, types, swipes, scrolls, presses keys, screenshots, dumps the view hierarchy, checks the host, installs drivers |
navigation_memory_ |
5 | Answers "where am I", "how do I get from here to there", searches remembered screens, replays a run, forgets one |
The navigation memory is not a separate step. When it is enabled — it is by
default — it wraps the Appium session and gesture ports at the composition root,
so every interaction records itself with no extra tool call. The cost is one
extra page-source capture per gesture; AT_NAVIGATION_MEMORY_RECORD_INTERACTIONS=false
turns it off and gives that latency back, leaving the read tools working against
whatever was learned before.
Prerequisites
The server itself is pure Python, but it is a remote control for other people's tools. What you need depends on which tools you intend to call — you can install just the Android half, or just the Appium half, and the rest still works.
| For | You need | Checked by |
|---|---|---|
| Anything | Python 3.12+ | — |
android_* |
The Android SDK, with adb, emulator, avdmanager and sdkmanager. Set ANDROID_SDK_ROOT |
android_get_all_devices |
| Booting an emulator | Hardware acceleration (KVM on Linux, Hypervisor.Framework on macOS) and a downloaded system image | android_get_installed_emulators |
appium_* |
Node.js 18+, the appium CLI, and the uiautomator2 driver |
appium_check_environment |
navigation_memory_* |
Nothing. It is a local SQLite file | — |
Call appium_check_environment once everything is in place — it reports exactly
what is still missing and how to install it, rather than failing on your first
gesture.
Supported platforms
| Platform | Status |
|---|---|
| Linux | Supported and tested |
| Windows | Not supported natively — use WSL2, below |
| macOS | Should work; POSIX throughout, but not tested by the maintainers |
Native Windows is not a packaging gap, it is a real one: the process handling in
infrastructure/*/process.py uses os.killpg, os.getpgid and
signal.SIGKILL, none of which exist on Windows, so every timeout and shutdown
path would fail there. WSL2 is the supported route, and it is a good one — see
Windows (WSL2).
Installing the prerequisites
Ubuntu / Debian
Tested on Ubuntu 24.04 LTS. Every block below can be pasted as-is.
1. Python and uv
sudo apt update
sudo apt install -y python3 python3-venv curl unzip
curl -LsSf https://astral.sh/uv/install.sh | sh
uv manages its own Python, so a distro Python older than 3.12 is not a problem
— uv will fetch 3.12 when it needs one.
2. A JDK
sdkmanager and avdmanager are Java programs and will not start without one.
JDK 17 or 21; 21 is what this project is developed against.
sudo apt install -y openjdk-21-jdk-headless
java -version
3. The Android SDK command-line tools
You do not need Android Studio. Note the latest/ directory in the last
step — sdkmanager refuses to run unless it sits at
cmdline-tools/latest/bin/sdkmanager, and unzipping the archive gives you
cmdline-tools/bin/ instead. Getting this wrong is the single most common
setup failure.
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
cd /tmp
curl -O https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
unzip -q commandlinetools-linux-13114758_latest.zip
# The archive unpacks to ./cmdline-tools/ — it has to end up under latest/
mv cmdline-tools "$ANDROID_SDK_ROOT/cmdline-tools/latest"
Check the newest build number at developer.android.com/studio under "Command line tools only" if that URL has moved on.
4. Set the environment variables
cat >> ~/.bashrc <<'EOF'
# Android SDK
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
export ANDROID_HOME="$ANDROID_SDK_ROOT"
export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$PATH"
EOF
source ~/.bashrc
5. SDK packages, and the licences
sdkmanager --licenses # answer y to each; nothing installs until you do
sdkmanager "platform-tools" "emulator" "platforms;android-34"
6. A system image
An AVD cannot be created without one, and this project never downloads one for
you — android_download_image exists precisely so that the download is an
explicit choice.
# google_apis (not playstore) pairs with any device profile
sdkmanager "system-images;android-34;google_apis;x86_64"
Use arm64-v8a instead of x86_64 on ARM hardware.
7. Hardware acceleration (KVM)
Without this an emulator boots by software emulation and is unusably slow — slow enough that it will exceed the boot timeout rather than merely annoy you.
# Does the CPU support it at all? A number greater than 0 means yes.
egrep -c '(vmx|svm)' /proc/cpuinfo
sudo apt install -y qemu-kvm
sudo usermod -aG kvm "$USER"
# Log out and back in, then confirm — this must print your username in the group:
ls -l /dev/kvm && groups | tr ' ' '\n' | grep -x kvm
If you are inside a VM, enable nested virtualisation on the host first.
8. Node.js, Appium and the driver
The distro's nodejs package is usually too old; nvm avoids sudo npm entirely.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
npm install -g appium
appium driver install uiautomator2
9. Verify
adb --version # SDK platform-tools
emulator -version # SDK emulator
avdmanager list device | head # the JDK works
appium --version
appium driver list --installed # uiautomator2 should be listed
[!WARNING] Do not
apt install adb. The distro package puts anadbon yourPATHthat is routinely a different major version from the SDK'semulator, and the two fight over the adb server — the symptom is devices appearing and vanishing at random. On the machine this project is developed on,/usr/bin/adbis34.0.4-debianwhile the SDK's is37.0.1. This server resolves SDK-local tools ahead ofPATHspecifically to survive that, but your own shell will not, so prefer the SDK'splatform-toolson yourPATHas step 4 does. If you already installed it:sudo apt remove adb.
Windows (WSL2)
Native Windows does not work — see Supported platforms for the specific reason. Run the whole stack inside WSL2 instead.
[!NOTE] These steps are the standard WSL2 route, but the maintainers develop on Linux and have not tested them end to end. The nested-virtualisation step in particular is the one that varies between machines. Corrections via issue or PR are very welcome.
1. Install WSL2 with Ubuntu
In PowerShell as Administrator:
wsl --install -d Ubuntu-24.04
wsl --update
Reboot when prompted, then set your Linux username and password.
2. Enable nested virtualisation, so the emulator can use KVM
This is the step that decides whether you get a usable emulator or a slideshow.
It needs Windows 11 (or Windows 10 build 19041+ with a recent WSL2). In
PowerShell, create or edit %UserProfile%\.wslconfig:
[wsl2]
nestedVirtualization=true
memory=8GB
processors=4
Then, back in PowerShell:
wsl --shutdown
Reopen Ubuntu and confirm KVM is really there:
egrep -c '(vmx|svm)' /proc/cpuinfo # must be > 0
ls -l /dev/kvm # must exist
If /dev/kvm is missing, nested virtualisation did not take effect and the
emulator will not be usable — fix that before going further.
3. Everything else, inside WSL2
Follow the Ubuntu / Debian steps above from inside the WSL2 shell. The Android SDK, Node, Appium and this server all live in WSL2, not on Windows.
Do not install the Android SDK on the Windows side and try to reach it from
WSL2 through /mnt/c. Windows .exe tools cannot be driven the way this server
drives them, and the path translation will bite you.
4. A physical Android phone over USB
WSL2 has no direct USB access, so a cable alone will not do. Either use an emulator inside WSL2, or share the device with usbipd-win:
winget install usbipd
usbipd list # find your phone's BUSID
usbipd bind --busid <BUSID>
usbipd attach --wsl --busid <BUSID>
Then adb devices inside WSL2 should list it.
5. Point your MCP client at WSL2
A client running on Windows must launch the server through WSL:
{
"mcpServers": {
"app-automating": {
"command": "wsl",
"args": ["-d", "Ubuntu-24.04", "--", "bash", "-lc", "uvx app-automating"]
}
}
}
bash -lc matters: it loads your login profile, which is where step 4 of the
Ubuntu instructions put ANDROID_SDK_ROOT. Without it the server starts and
reports no Android SDK on a machine that plainly has one.
macOS
Untested by the maintainers, but the code is POSIX throughout and nothing in it is Linux-specific.
brew install --cask temurin # a JDK
brew install --cask android-commandlinetools
brew install node
npm install -g appium
appium driver install uiautomator2
export ANDROID_SDK_ROOT="$(brew --prefix)/share/android-commandlinetools"
sdkmanager --licenses
sdkmanager "platform-tools" "emulator" "system-images;android-34;google_apis;arm64-v8a"
Use arm64-v8a on Apple Silicon and x86_64 on Intel. Acceleration is
Hypervisor.Framework and needs no setup.
Nothing here reaches the network except android_download_image, which shells out
to sdkmanager, and appium_install_driver, which shells out to npm.
Install
As a tool (recommended)
uv tool install app-automating
Or run it without installing anything permanent:
uvx app-automating
From the repository
For an unreleased commit:
uvx --from git+https://github.com/dkmostafa/app-automating app-automating
From a clone, for development
git clone https://github.com/dkmostafa/app-automating
cd app-automating
uv sync
uv run app-automating
Connect it to an MCP client
The server speaks STDIO and only STDIO. It refuses every other transport at the source, so it cannot be accidentally exposed on a socket — the tools it fronts drive every attached device with no authentication of any kind.
Claude Code
claude mcp add app-automating -- uvx app-automating
Claude Desktop, Cursor, and other clients that read a JSON config
Add this to the client's MCP configuration file:
{
"mcpServers": {
"app-automating": {
"command": "uvx",
"args": ["app-automating"],
"env": {
"ANDROID_SDK_ROOT": "/home/you/Android/Sdk"
}
}
}
}
From a clone instead of an install:
{
"mcpServers": {
"app-automating": {
"command": "uv",
"args": ["--directory", "/path/to/app-automating", "run", "app-automating"],
"env": {
"ANDROID_SDK_ROOT": "/home/you/Android/Sdk"
}
}
}
}
ANDROID_SDK_ROOT is worth setting explicitly. An MCP client launches the server
without your shell profile, so a variable that is set in .bashrc will not be
there — which shows up as "no Android SDK" on a machine that plainly has one.
Configuration
Every setting is an environment variable prefixed AT_, and every one has a
working default. .env.example documents all of them, with
the reasoning for each default; copy it to .env and edit if you want to pin
something.
The settings you are most likely to touch:
| Variable | Default | What it is for |
|---|---|---|
ANDROID_SDK_ROOT / ANDROID_HOME |
auto-detected | Where the Android SDK lives |
AT_EMULATOR_LAUNCH_ARGS |
empty | Flags added to every emulator launch, e.g. -gpu swiftshader_indirect on a machine with no usable GPU |
AT_EMULATOR_BOOT_TIMEOUT_SECONDS |
300 |
How long to wait for a booting AVD |
AT_APPIUM_MANAGE_SERVER |
true |
Launch an Appium server when nothing is listening, and kill it on shutdown |
AT_APPIUM_SERVER_PORT |
4723 |
Where the Appium server listens, on loopback only |
AT_NAVIGATION_MEMORY_DATABASE_PATH |
$XDG_DATA_HOME/app-automating/navigation_memory.db |
The navigation memory. :memory: for one that dies with the process |
AT_NAVIGATION_MEMORY_RECORD_INTERACTIONS |
true |
Whether every gesture records itself |
Configuration is read in exactly one place per module — the composition root — and handed down explicitly. Nothing deeper in the code reads the environment.
How it is built
Three modules under src/app_automating/modules/, one per surface, each a
vertical slice split into the same four layers with a fixed direction of
dependency:
presentation -> application -> infrastructure -> domain
\____________________________________________^
domain/— entities, the failure vocabulary, and the ports. No I/O, no frameworks, importable on a machine with no Android SDK.infrastructure/— the adapters: host binaries, devices, the filesystem, the database.application/— the services, and thedi.pythat wires them. The only layer allowed to name a concrete adapter.presentation/— the module's own MCP tools, their schemas, their rendering and their error translation.
src/app_automating/server.py is the server and nothing else: it owns the single
STDIO FastMCP instance, builds each module's services through that module's
di.py, registers the module's tools, and defines no tool of its own.
This is not decoration — it is enforced. Each module carries a
tests/unit/test_architecture.py that walks the AST of every file in it and
fails the build on a layer violation, a missing tool docstring section, an
unregistered module, or a mock in the wrong place. The rules themselves are
written down in CLAUDE.md and .claude/rules/.
Tests
uv run pytest -m unit # pure code: no SDK, no device, <1s
uv run pytest -m integration # the real toolchain: adb, avdmanager, Appium, a real boot
uv run pytest # everything
Tests live beside the code they cover, one test file per source file — there is
no top-level tests/ tree.
[!WARNING] The integration suite drives this host for real. It creates AVDs, boots emulators, starts Appium servers and writes real files. It namespaces everything it creates, cleans up after itself even after a failure, and fails the run if it touched something it did not own — but it will take over your display and cost a minute of your machine. It never downloads anything, and it skips with a reason rather than failing on a host that cannot run it.
CI runs the unit and architecture tests only. There is no Android SDK and no device on a GitHub runner, so the integration tests skip there by design — they are yours to run.
Contributing
See CONTRIBUTING.md. The short version: the four-layer rules
are enforced by tests rather than by review, so read
.claude/rules/ before adding a layer, a component, or a
dependency between two of them.
Security
An Appium server drives every attached device with no authentication of any kind, which is why this server binds it to loopback and speaks STDIO only. To report a vulnerability, see SECURITY.md.
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file app_automating-0.1.1.tar.gz.
File metadata
- Download URL: app_automating-0.1.1.tar.gz
- Upload date:
- Size: 332.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
39e967ad1d410e1c46b66c58f714894cae4e942fd49901befd50f607cbdb08ac
|
|
| MD5 |
9998d6d180276600d5cd982be7a6dd81
|
|
| BLAKE2b-256 |
f4fd927d5f4323a745dd5147e903a6f2a8ac06fca194e0f3375bb8c575646f38
|
Provenance
The following attestation bundles were made for app_automating-0.1.1.tar.gz:
Publisher:
release.yml on dkmostafa/app-automating
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
app_automating-0.1.1.tar.gz -
Subject digest:
39e967ad1d410e1c46b66c58f714894cae4e942fd49901befd50f607cbdb08ac - Sigstore transparency entry: 2752524844
- Sigstore integration time:
-
Permalink:
dkmostafa/app-automating@7915c05ff376825780550ac64035255aa29d9722 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dkmostafa
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7915c05ff376825780550ac64035255aa29d9722 -
Trigger Event:
push
-
Statement type:
File details
Details for the file app_automating-0.1.1-py3-none-any.whl.
File metadata
- Download URL: app_automating-0.1.1-py3-none-any.whl
- Upload date:
- Size: 230.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25758e74f5468c427f0ea145c3834ae7eef889f8b630ca7b5c956a3b6f20f050
|
|
| MD5 |
e0a14ab8af3008aba553a2f032f9b346
|
|
| BLAKE2b-256 |
f61a07c6fc2fd0d1c811133ac802702e13cd9d809a9ad9beeb04e43718862ffc
|
Provenance
The following attestation bundles were made for app_automating-0.1.1-py3-none-any.whl:
Publisher:
release.yml on dkmostafa/app-automating
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
app_automating-0.1.1-py3-none-any.whl -
Subject digest:
25758e74f5468c427f0ea145c3834ae7eef889f8b630ca7b5c956a3b6f20f050 - Sigstore transparency entry: 2752524851
- Sigstore integration time:
-
Permalink:
dkmostafa/app-automating@7915c05ff376825780550ac64035255aa29d9722 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dkmostafa
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7915c05ff376825780550ac64035255aa29d9722 -
Trigger Event:
push
-
Statement type: