Shared Android Instrumented Runner: Enables junit5 tests to be run by multiple orgs / repos etc. on a shared set of android devices
- ✓Open-source license (Apache-2.0)
- ✓Actively maintained (<30d)
- ✓Clear description
- ✓Topics declared
- ✓Documented (README)
- !Install pipes a remote script into a shell (curl | sh)
git clone https://github.com/compscidr/sair{
"mcpServers": {
"sair": {
"command": "sair",
"env": {
"SAIR_API_KEY": "<sair_api_key>"
}
}
}
}SAIR_API_KEYMCP Servers overview
# SAIR — Shared Android Instrumented Runner
SAIR lets CI pipelines safely share physical Android devices. It provides
device locking, ADB protocol translation, and per-runner isolation so multiple
jobs never collide on the same device.
## Why
Without SAIR, every CI job that can see a phone uses it at the same time:
```
CI job 1 ──┐
├──▶ adb ──▶ Phone ✗ both jobs install / uninstall the same APK
CI job 2 ──┘ ✗ one job's reboot or settings change breaks
the other's tests mid-run
✗ connectedCheck runs on every attached
phone, including ones another job is using
```
With SAIR, a job must hold a lock before it sees a device, and waiting jobs
queue:
```
CI job 1 ──┐ ┌──▶ Phone A (locked by job 1)
├──▶ Proxy ───────┤
CI job 2 ──┘ ▲ └──▶ Phone B (locked by job 2)
│ locks
CI job 3 ··· Orchestrator: job 3 waits in the queue until
a phone is released
```
## GitHub Action
This repo is also a GitHub Action that puts `sair-acquire`, `sair-release` and
`sair-wifi` on `PATH`. The tools need an orchestrator account: sign up at
[sair.run](https://sair.run) to get an API key, store it as the repository
secret `SAIR_API_KEY`, and set the repository variable `SAIR_PROXY_URL` to
your bench's proxy (see [Run](#run)). Then:
```yaml
steps:
- uses: actions/checkout@v4
- uses: compscidr/sair@v0.1.18
- run: sair-acquire --count 1 > /dev/null
env:
SAIR_PROXY_URL: ${{ vars.SAIR_PROXY_URL }}
SAIR_API_KEY: ${{ secrets.SAIR_API_KEY }}
- run: ./gradlew connectedCheck
- if: always()
run: sair-release --status ${{ job.status }}
env:
SAIR_API_KEY: ${{ secrets.SAIR_API_KEY }}
```
The [full workflow example](#example-github-actions-workflow) explains each
step.
## AI agents (MCP)
`sair-mcp` is an MCP server (stdio) for coding agents. It can run an app's
instrumented tests on a real phone in one call, or lease a phone and install
APKs, run shell commands, take screenshots and read logcat on it. It talks to
the orchestrator's agent API with an API key from [sair.run](https://sair.run).
The agent's machine needs no proxy or adb.
Install it with `go install github.com/compscidr/sair/cmd/sair-mcp@latest`,
take `sair-mcp` from a release tarball, or use `sair-mcp.mcpb` from a release
(an [MCP Bundle](https://github.com/modelcontextprotocol/mcpb) for clients
such as Claude Desktop). Then add it to your agent, e.g. Claude Code:
```bash
claude mcp add sair -e SAIR_API_KEY=sair_... -- sair-mcp
```
or any client's JSON config:
```json
{ "mcpServers": { "sair": { "command": "sair-mcp", "env": { "SAIR_API_KEY": "sair_..." } } } }
```
| Tool | Does |
|------|------|
| `run_tests` | Uploads `app_apk` and `test_apk` (paths on the agent's machine), waits for the run, returns per-test results, instrument output and a logcat tail |
| `list_devices` | Lists the phones the key may use: serial, model, `sdk` (API level), shared, in use |
| `lease_device` | Takes a phone (`serial`, `wait_s` optional) and returns the lease id |
| `install_apk` | Installs an APK on the leased phone, returns its package |
| `shell` | Runs a command, returns output and exit code |
| `screenshot` | Returns a PNG of the screen |
| `logcat` | Returns the most recent `lines` (default 500) |
| `release_device` | Ends the lease: uninstalls what it installed and frees the phone |
The server heartbeats the leases it takes, so an agent that thinks for a while
between calls keeps its phone. A lease still ends at the plan's maximum
(free 15 min, solo 1 h, team 2 h, enterprise 4 h). On a phone shared by another
user, shell commands are limited to a safe set (see the orchestrator README).
`SAIR_API_URL` points it at another orchestrator (default
`https://api.sair.run`).
It is listed in the [official MCP registry](https://registry.modelcontextprotocol.io)
as `io.github.compscidr/sair`. Every release publishes it: the release
workflow builds `sair-mcp.mcpb`, fills `server.json`'s version, download URL
and SHA-256 (the values in the repo are placeholders), and publishes it with
`mcp-publisher`, logged in through GitHub OIDC. Check it with
`curl "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.compscidr/sair"`.
## Quick Start
### Install
```bash
curl -fsSL https://raw.githubusercontent.com/compscidr/sair/main/install.sh | bash
```
This detects your OS and architecture, downloads the latest release, and
installs all binaries to `~/.local/bin`.
### Prerequisites
- `adb` installed on the machine with Android devices
- One or more Android devices connected via USB with USB debugging enabled
(don't sign them in to a personal account: see
[Securing a bench](#securing-a-bench))
### Run
```bash
# 1. Start a real ADB server on a non-default port (5038), so it doesn't
# conflict with the proxy which will own the standard port (5037).
adb -P 5038 start-server
# 2. Set your API key (shared by proxy and device-source)
export SAIR_API_KEY=your-api-key
# 3. Start the proxy and device source
sair-proxy &
sair-device-source
```
The proxy connects to the hosted orchestrator at `orchestrator.sair.run` by
default (with TLS). Override with `ORCHESTRATOR_ADDR` for local development.
### Use
Acquire a device lock (blocks until one is available), run your tests, then
release:
```bash
ACQUIRE_OUTPUT=$(sair-acquire)
eval "$ACQUIRE_OUTPUT"
./gradlew connectedCheck
sair-release
```
After eval, stock `adb` automatically talks to the proxy through the scoped
port and only sees the locked devices.
When the bench sets `WIFI_SSID` on the device source, Wi-Fi is managed for
you:
- **On acquire** each phone joins the bench Wi-Fi and the acquire returns only
once the phone is *online* (below); it fails otherwise. `--no-wifi` hands it
over *offline* instead, verified the same way rather than trusting the
previous release.
- **On release**, and when a lock expires or is cancelled, each phone is reset:
every suggestion added through `adb shell` and every saved network is
forgotten and the radio is toggled, and the reset waits until the phone is
offline with the radio back on. The next job never inherits the last one's
network.
- **Also on release**, every third-party package the phone did not have when
the device source first saw it is uninstalled (test APKs and `.test`
packages included, with their app data and signed-in accounts), and the
browser and mail apps are force-stopped; see `BASELINE_PACKAGES`,
`RESET_FORCE_STOP` and `RESET_CLEAR_PACKAGES`. What was removed is written to
the proxy log and the lock's log on the orchestrator, naming any Wi-Fi
suggestion that went with a package. This runs before the Wi-Fi reset, so a
suggestion a test app leaked for the bench network is gone before the reset
waits for the phone to be offline.
- **On acquire** the same cleanup runs first (reverse forwards, packages),
before the Wi-Fi check. A job that never released, because it had no
`if: always()` release step, its runner died, or the proxy was down when
the lock expired, is cleaned up by the next job, whose lock log then shows
`sair: uninstalled … (left by an earlier job)`.
- **Awake and unlocked**, on acquire and again after the release reset: the
screen is woken and the keyguard dismissed, then verified (`dumpsys power`
says `Awake`, `dumpsys window policy` says the keyguard is not showing),
with up to three attempts since a one-shot dismiss can race on some phones.
A phone whose keyguard stays up fails the acquire with `screen locked`; that
means a PIN, pattern or password is set, which sair cannot dismiss, so set
the phone's screen lock to Swipe or None. The reason is also reported as
the device's `warning` (proxy `/status`, device report) until a wake
succeeds. A phone with `stay_on_while_plugged_in` off is logged as a
warning, since it will sleep and re-lock between steps. Android 4.x phones
(no `wm`) are verified through their own `mShowingLockscreen` field; a dump
in a format sair does not know is logged once and treated as unlocked.
- **Rebooted on release** once its uptime is over `REBOOT_UPTIME_HOURS`
(24 by default), while the phone is unacquired: the reset waits for the
boot and the wake above before the next acquire of that phone can proceed,
and the phone is not offered while it is down. The lock log gets
`sair: rebooted after release (uptime …)` and the proxy's `/status` lists
each phone's `last_reboot`. This runs before the Wi-Fi reset, so the
offline wait is not spent twice. A phone with a PIN, pattern or password
is never rebooted this way, since it would come back on "unlock to start"
and need a human; the device source logs that with the fix. The wake and
the reboot run whether or not the bench manages Wi-Fi.
A bench that was already running jobs has phones carrying the last job's
packages, so "first seen" would record those into the baseline. A test package
and its target, where `pm list instrumentation` names both and both are
installed, are never baselined, which covers CI leftovers without touching
apps that merely ship an instrumentation of their own; for anything else, or to
be certain, set
`BASELINE_PACKAGES` explicitly after one look at `pm list packages -3` on each
phone (set it empty to keep no third-party package at all). The device source
logs the baseline it recorded, by name, so an unwanted entry is visible in the
first log after the upgrade.
- **Mid-job**, `sair-wifi off` / `sair-wifi on` drop and restore the network
without the job knowing the credentials, and return only once the phone is
offline or online. Each phone is switched on its own, so two phones can ask
for opposite states at the same time.
- **From the phone**, a test can toggle its own Wi-Fi between two assertions
with the lock's device tokeWhat people ask about sair
What is compscidr/sair?
+
compscidr/sair is mcp servers for the Claude AI ecosystem. Shared Android Instrumented Runner: Enables junit5 tests to be run by multiple orgs / repos etc. on a shared set of android devices It has 1 GitHub stars and its last recorded update is dated 2026-10-16.
How do I install sair?
+
You can install sair by cloning the repository (https://github.com/compscidr/sair) or following the README instructions on GitHub. ClaudeWave also provides quick install blocks on this page.
Is compscidr/sair safe to use?
+
Our security agent has analyzed compscidr/sair and assigned a Trust Score of 87/100 (tier: Trusted). See the full breakdown of passed checks and flags on this page.
Who maintains compscidr/sair?
+
compscidr/sair is maintained by compscidr. The last recorded GitHub activity is dated 2026-10-16, with 2 open issues.
Are there alternatives to sair?
+
Yes. On ClaudeWave you can browse similar mcp servers at /categories/mcp, sorted by popularity or recent activity.
Deploy sair to your cloud
Ship this repo to production in minutes. Each platform spins up its own environment with editable env vars.
Maintain this repo? Add a badge to your README
Drop the badge into your GitHub README to show it's tracked on ClaudeWave. Each badge links back to this page and reflects the live Trust Score.
[](https://claudewave.com/repo/compscidr-sair)<a href="https://claudewave.com/repo/compscidr-sair"><img src="https://claudewave.com/api/badge/compscidr-sair" alt="Featured on ClaudeWave: compscidr/sair" width="320" height="64" /></a>More MCP Servers
Reverse engineer anything with agents, from app behavior down to native binaries.
Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.
User-friendly AI Interface (Supports Ollama, OpenAI API, ...)
An open-source AI agent that brings the power of Gemini directly into your terminal.
Real-time global intelligence dashboard. AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface
🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl! Don't be shy, join here: https://discord.gg/EMgGbDceNQ and follow here for daily tips and tricks: https://x.com/Scrapling_dev