Swipium
<p align="center">
<img src="docs/assets/swipium-lockup-ink-on-light.png" alt="Swipium" width="420">
</p>
# Swipium
An MCP server that lets coding agents QA mobile apps on Android Emulators and iOS Simulators.
[](https://www.npmjs.com/package/swipium)
[](LICENSE)
[](https://nodejs.org)
[](https://modelcontextprotocol.io)
[](https://swipium.com)
Swipium gives an AI coding agent (Claude Code, Codex, Gemini CLI, Cursor, VS Code, and other MCP clients) the tools a QA engineer uses: find or build the app, boot a simulator, install and launch it, read the screen, tap and type through real user flows, and write a report backed by screenshots, logs, and UI dumps. A run can be turned into a repeatable flow, a test suite, or Appium code.
It is for mobile developers who want their agent to catch the broken login screen before a TestFlight or Play Console build. It runs locally (stdio; the server opens no network port, and the WebDriverAgent it manages for iOS takes commands on 127.0.0.1 only, though its screen stream port is reachable from the network; see [Security](#security)), drives devices through `adb`, `simctl`, and WebDriverAgent rather than Appium, and asks your consent before anything with side effects. [swipium.com](https://swipium.com)
### A session, abbreviated
```text
You: Smoke test this app on Android with Swipium.
Agent > qa_test_this { goal: "smoke", waitForCompletion: true } (runs by default)
🔐 Consent required (low): • boot_emulator: emulator -avd Pixel_8_API_35 -no-audio -no-snapshot-save -no-boot-anim -gpu auto -no-window
• install_apk: adb -s <Pixel_8_API_35 once booted> install -r -g android/app/.../app-release.apk
You: Approve (one prompt covers boot + install)
state: "running", sessionId: "3f9c2a1b", jobId: "a41c09e2"
Agent > qa_job_status { sessionId: "3f9c2a1b", jobId: "a41c09e2", waitMs: 45000 }
status: "done", result.state: "completed"
reportHead: "RELEASE RISK: SHIP … Health: native OK, app OK … Open the report: …/report/report.html"
nextRecommendedAction: qa_get_artifact { uri: "swipium://session/3f9c2a1b/report/…-summary.md" }
Agent: The app launched and passed the smoke checks with no crashes or error screens.
```
Three calls: start the run, wait for it, and (only when the inlined head isn't enough) read `report.md`. The report behind that summary, rendered with `npx swipium report --latest --format markdown`, includes:
```markdown
**Release risk: 🟢 SHIP**
**App status:** PASS - No high-severity app/native finding observed in this run.
**Coverage status:** COVERED - Structured smoke/workflow evidence was collected.
**Tool status:** PASS - No Swipium/MCP limitations or tool errors recorded in this run.
```
## Contents
[Requirements](#requirements) · [Quickstart](#quickstart) · [Starter prompts](#starter-prompts) · [How it works](#how-it-works) · [Tools](#tools) · [Where results go](#where-results-go) · [Configuration](#configuration--environment-variables) · [CLI](#cli-reference) · [CI](#ci) · [Upgrading](#upgrading) · [Troubleshooting](#troubleshooting) · [Security](#security)
## Requirements
**Node.js 22.12 or newer.** On an older Node.js the server prints a warning on stderr and `swipium verify` fails. Swipium works with emulators and simulators only; physical devices are refused with `PHYSICAL_DEVICE_UNSUPPORTED` ([why](docs/physical-devices.md)). Structured iOS automation needs WebDriverAgent 6.0 or newer (`qa_wda {action:"setup"}` builds and starts one); without it an iOS Simulator is visual-only.
**MCP:** a stdio server that speaks protocol revisions 2025-06-18, 2025-11-25 and 2026-07-28; the client's first message picks one. Claude Code 2.1.285 and later connect on 2026-07-28, Codex 0.146 on 2025-06-18. Any stdio MCP client should work ([protocol versions](docs/mcp-server.md#protocol-versions)).
| Host | Android Emulator | iOS Simulator |
| --- | --- | --- |
| macOS | Supported | Supported (Xcode with a Simulator runtime) |
| Linux | Supported | Not available |
| Windows | Experimental and untested; some process-cleanup helpers rely on `ps` | Not available |
| Your app | What Swipium needs |
| --- | --- |
| React Native / Expo, debug build | Metro serving this project's JS bundle. `qa_metro` can start it (consent-gated); otherwise the run stops with `METRO_REQUIRED`, on Android and for iOS simulator builds without an embedded bundle alike. A Metro on port 8081 serving another project is `METRO_WRONG_PROJECT`. Expo dev-client builds must be connected to Metro by hand for now; when this project's Metro is already serving, the blocker gives the dev-client deep link to open. |
| React Native / Expo release, or native Android / iOS | An installable artifact (APK or `.aab` for Android, a simulator `.app` for iOS; a device `.ipa` is refused) or a project Swipium can build. |
| Expo managed (no `android/` or `ios/`) | Nothing extra: the build (`npx expo run:android --device <AVD>` or `npx expo run:ios`, consent-gated) generates the native project first. If that prebuild step fails, the build fails with `EXPO_PREBUILD_REQUIRED`; run `npx expo prebuild` yourself and retry. |
| Only a built app, no source | Put the simulator `.app` or the `.apk` in a directory and pass that directory as `projectRoot` ([Testing a prebuilt build](docs/concepts.md#project-root)). |
| Flutter | A buildable Flutter project, or a built APK / `.app`. |
- **Android:** platform-tools, the Emulator, and at least one AVD (usually via Android Studio). `adb` on your `PATH` is used if present, else the SDK copy from `$ANDROID_HOME`, `$ANDROID_SDK_ROOT`, or the default SDK location (`~/Library/Android/sdk`, `~/Android/Sdk`, `%LOCALAPPDATA%\Android\Sdk`). The `emulator` binary that boots AVDs and `aapt2` come from the SDK first. `.aab` files need bundletool.
- **iOS (macOS only):** Xcode and a simulator. Taps, typing, and UI-tree snapshots need **WebDriverAgent** (WDA, the on-simulator automation server Appium uses); without it iOS is visual-only ([iOS modes](docs/concepts.md#ios-modes)).
## Quickstart
**1. Register Swipium with your client.** From your app repository, for Claude Code:
```bash
npx -y swipium init claude --scope project # preview; changes nothing
npx -y swipium init claude --scope project --apply # writes .mcp.json and the skill, then runs `swipium verify`
```
For Claude Code, `init` also sets `SWIPIUM_MAX_WAIT_MS=110000` (one call can wait up to 110 s, so most runs finish in one call) and installs the **swipium-qa agent skill** under `.claude/skills/` (`~/.claude/skills/` for `--scope user`; `--no-skill` skips it). `--trust-local` (local or user scope only) also pre-approves the consent actions that run no project code, so boots and installs stop prompting while builds, Metro and WebDriverAgent still ask. A Claude Code plugin with the same server and skill is in [`plugin/`](plugin/README.md). Swipium is also listed in the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.GeroPalombo/swipium`, so registry-aware clients can find and install it.
The same command handles `codex`, `gemini`, `cursor`, and `vscode` (`swipium init <client> --apply`). Claude Desktop, Windsurf, manual configs, and per-client details are in **[docs/mcp-server.md](docs/mcp-server.md)**.
**Codex:** `swipium init codex --apply` appends a `[mcp_servers.swipium]` block to `~/.codex/config.toml` (or `$CODEX_HOME/config.toml`) with longer timeouts and an `env_vars` line, and installs the skill under `.agents/skills/`. Codex doesn't pass your shell environment to MCP servers, so without that line `SWIPIUM_TEST_*`, `ANDROID_HOME` and `JAVA_HOME` never reach Swipium. If you already have a block, `init` prints the line to add. Keep `tool_timeout_sec = 600` ([Codex setup](docs/mcp-server.md#codex)).
**Headless runs** (`codex exec`, `claude -p`, CI): these clients answer consent prompts automatically, so nothing gated (boot, install, build) runs unless you pre-approve exact actions with `SWIPIUM_CONSENT_PREAPPROVE` in the server env; approving in the chat doesn't help. Pre-approval isn't tied to a device, so on a machine with several emulators or simulators also pin one with `SWIPIUM_DEVICE` ([Headless runs](docs/mcp-server.md#headless-runs)).
**2. Restart the client** and check that it lists `qa_test_this`, `qa_doctor`, and `qa_report`.
**3. Ask the agent to test the app:**
```text
Use Swipium to smoke test this app: qa_test_this with goal "smoke" and
waitForCompletion true, poll qa_job_status while it is running, and
summarize the report.
```
`qa_test_this` runs by default (`mode:"execute"`, since 2.3.0); pass `mode:"plan"` for a side-effect-free preview. `goal:"smoke"` is the fastest path. With no `goal`, `qa_test_this` runs the smoke check and then tries to generate a test suite. With the skill installed, "test this app with Swipium" is enough.
If a tool returns `PROJECT_ROOT_UNRESOLVED`, name the absolute project path in your prompt or set `SWIPIUM_PROJECT_ROOT` in the server config ([Project root](docs/concepts.md#project-root)).
### What you'll see
- **One approval prompt** listing the exact build, boot, and install commands. Clients with MCP elicitation show a real prompt; others return `requiresConsent`, which the agent must relay to you.
- **The first build can take minutes.** The run is a background job. `waitForCompletion` waits up to about 45 s by default (about 105 s with the `SWIPIUM_MAX_WAIT_MS=110000` that `init claude` sets). A `qa_job_status` poll returns at once unless the agent passes `waitMs`; Swipium tells it to pass 45000, and a poll is capped at 50 s (110 s with that setting).
- **Two states.** The job `status` is `running`, `done`, `failed`, or `cancelled`. The run's `result.state` is `completed` or `needs_input` (job `done`), or `blocked` or `unsafe` (job `failed`). `needs_input` means one question for you, such as login credentials.
- **A report** with separate verdicts for the app, coverage, and Swipium itself. A completed run inlines a short head (verdict, failures with their failing step and screenshot URI) and saves `report.md` for the agent; a blocked run names the fix. For you there is `report.html`: one self-contained page with the verdict, each failure with its screenshot and log, and the step timeline, which opens offline in any browser (the agent gives you its path, `reportHtmlPath`; [what's in it](docs/concepts.md#whats-in-a-report)).
<img src="docs/assets/report-html.png" alt="Swipium report.html for a failed run: a red Release gate: BLOCK banner with the reason and next action, then the failing checkout workflow with its screenshot of a React Native red screen, failing step #6 tap "Pay" and code REDBOX" width="720">
## Starter prompts
| Goal | Prompt |
| --- | --- |
| Smoke test | "Use Swipium to smoke test this app: qa_test_this with goal smoke and waitForCompletion, then summarize the report." |
| Test login | "Use Swipium to test login with goal test_login. Don't ask me for the password: type `${SWIPIUM_TEST_EMAIL}` and `${SWIPIUM_TEST_PASSWORD}` with qa_act." (set both in the server `env`; placeholders expand server-side and secret values are redacted) |
| Reproduce a bug | "Use Swipium to reproduce this bug with goal reproduce_bug and goalText: 'checkout button does nothing after adding a coupon'. Attach the evidence." |
| Save a flow | "Turn the last Swipium run into a flow named login-smoke with qa_generate and save it." |
| Release gate | "Run Swipium with goal release_gate and tell me whether the release gate passes." |
Clients that support MCP prompts can use the built-in ones instead: `swipium_setup_check`, `swipium_guardrail_validation`, `swipium_full_smoke`, `swipium_bug_repro`, `swipium_test_feature`, `swipium_generate_suite`, and `swipium_convert_run_to_flow`.
## How it works
```text
qa_test_this {waitForCompletion:true} > consent > job
│
qa_job_status {waitMs} <─── still running ───┘
├─ completed > reportHead inline; more in report.md (qa_get_artifact)
├─ blocked / unsafe > the fix in nextRecommendedAction
└─ needs_input > ask you one question > qa_continue_from_blocker (resumes the run)
```
`qa_test_this` finds or builds the app, boots a simulator, installs and launches the app, runs a smoke check (plus exploration or suite generation, per `goal`), and always writes a report. Failures carry a `failureCode` that `qa_explain_blocker` explains. For hands-on work the agent opens a session (`qa_start_session`), reads the screen with `qa_snapshot`, acts on element refs such as `@e3` (stable while the element stays on screen), and batches 3 or more known steps in one `qa_flow_run {flowYaml}`.
Concepts, explained in **[docs/concepts.md](docs/concepts.md)**:
- **[Project root](docs/concepts.md#project-root):** the repository under test, from the `projectRoot` argument, MCP roots, `SWIPIUM_PROJECT_ROOT`, `CLAUDE_PROJECT_DIR`, or the server's working directory.
- **[Consent](docs/concepts.md#consent):** builds, boots, installs, data wipes, network changes, and repo-supplied commands need your single-use approval.
- **[Sessions and jobs](docs/concepts.md#sessions-and-jobs):** a session holds one run's device, app, and evidence; long work is a job you poll.
- **[Secrets and redaction](docs/concepts.md#secrets-and-redaction):** credentials are redacted from text output; screenshots are not.
- **[iOS modes](docs/concepts.md#ios-modes):** visual-only through `simctl`, or full interaction with WDA.
- The **app map** is Swipium's durable memory of your app's screens; a **flow** is a replayable YAML script ([docs/flows.md](docs/flows.md)); a **POM suite** is a generated page-object test suite. See the [glossary](docs/concepts.md#glossary).
## Tools
Grouped by capability (`qa_status` without arguments returns the same groups). Every tool's parameters and behavior are in **[docs/tools.md](docs/tools.md)**; `swipium verify` lists what your installed version exposes.
| Group | Tools |
| --- | --- |
| Start | `qa_test_this`, `qa_status`, `qa_job_status`, `qa_job_cancel`, `qa_explain_blocker`, `qa_continue_from_blocker`, `qa_get_artifact` |
| Setup | `qa_doctor`, `qa_start_session`, `qa_prepare_target`, `qa_prepare_ios_target`, `qa_ios`, `qa_wda` |
| Build | `qa_resolve_target`, `qa_resolve_artifact`, `qa_build`, `qa_bundletool` |
| Device | `qa_device_info`, `qa_orientation`, `qa_geolocation`, `qa_network`, `qa_metro`, `qa_app_control`, `qa_screen_record` |
| Drive | `qa_snapshot`, `qa_inspect`, `qa_act`, `qa_clear_overlay`, `qa_check_health`, `qa_screenshot`, `qa_note`, `qa_visual`, `qa_wait` |
| Run | `qa_smoke`, `qa_explore`, `qa_report` |
| App map | `qa_app_map_build`, `qa_app_map_read`, `qa_app_map_query`, `qa_app_map_feature_scope`, `qa_app_map_update` |
| Feature | `qa_test_feature` |
| Flows | `qa_flow_check`, `qa_flow_run`, `qa_flow_compile`, `qa_flow_repair` |
| Generate | `qa_generate` |
| Test suite | `qa_suite_read`, `qa_suite_update`, `qa_suite_generate`, `qa_suite_export`, `qa_suite_lint` |
| Issues | `qa_issue_log`, `qa_mobile_audit` |
| First run | `qa_first_run` |
Every tool carries MCP annotations: read-only tools declare `readOnlyHint:true` and `openWorldHint:false` (so clients can auto-approve them); the rest also declare `destructiveHint` and `idempotentHint`.
## Where results go
- **`<your repo>/.swipium/`:** app map, flows, test suite, issue ledger, visual baselines, generated files, the WebDriverAgent build cache (`cache/wda-derived-data`, about 200 MB per project; `ios.wda.derivedDataPath` moves it), and your configuration (`config.json`, `fixtures.json`, `policy.json`). `swipium scan` merges its results into `config.json` and keeps every key you set (WDA team, OCR and mask commands, `appId`); it never regenerates the file. An `appId` counts as yours when it has no `appIdSource`, or `appIdSource:"config.json"`, or when it differs from `scanAppId` (the id the last scan detected, which scan stores next to it); then it is kept even with a scan `appIdSource` left in place, and `appIdSource` becomes `config.json`. One an earlier scan wrote and you didn't change is refreshed. A config from a scan older than 2.3.0 has no `scanAppId`: there a hand-edited `appId` with a scan `appIdSource` is still replaced, and scan prints `replaced appId <old> in config.json (written by an earlier scan from <source>; ...)`, so remove `appIdSource` to keep yours. The first time Swipium writes the app map, issue ledger, or WDA build cache in a Git repository, it adds `.swipium/` to `.gitignore`. Never pruned automatically.
- **`~/.swipium/runs/`:** per-session state and evidence (screenshots, logs, UI dumps, reports), returned as `swipium://` URIs. When a server process first loads its session registry, it prunes in the background (for at most 2 s per start) session directories older than `SWIPIUM_RETENTION_DAYS`, keeping registered sessions and the newest `SWIPIUM_RETENTION_KEEP` per project whose root still exists. `swipium gc` cleans up on demand. Session files are 0600 in 0700 directories, and so are `~/.swipium`'s `registry.json`, `projects.json` and `processes.json`.
## Configuration & environment variables
Set these in the MCP server's `env` block (or your shell, for CLI commands). This is the complete list of variables Swipium reads.
**Codex** forwards only the names listed in `env_vars` (plus a fixed whitelist such as `HOME` and `PATH`); `swipium init codex` writes that line for you. It deliberately leaves out `CLAUDE_PROJECT_DIR`, `SWIPIUM_DISABLE_DEVICE_DISCOVERY` and the approval grants (`SWIPIUM_CONSENT_PREAPPROVE`, `SWIPIUM_CONSENT_PREAPPROVE_RUN_CODE`, `SWIPIUM_ALLOW_REMOTE_WDA`): set those literally in `env = { ... }` so an inherited shell export or a direnv file in a cloned repo can't grant approvals. Add your own custom `SWIPIUM_*` flow variables (and `SWIPIUM_DEVICE`, if you pin a device) to the list. Details: [docs/mcp-server.md](docs/mcp-server.md#codex).
| Variable | Purpose |
| --- | --- |
| `SWIPIUM_PROJECT_ROOT` | Absolute path of the app repository when the client provides no MCP roots. |
| `CLAUDE_PROJECT_DIR` | Set by Claude Code; used as the project root after `SWIPIUM_PROJECT_ROOT`. |
| `ANDROID_HOME`, `ANDROID_SDK_ROOT` | Android SDK location, searched for `adb`, `emulator`, and `aapt2` (see [Requirements](#requirements)). |
| `BUNDLETOOL_JAR` | Path to `bundletool.jar` for converting `.aab` files. A `bundletool` launcher on `PATH` also works. |
| `DEVELOPMENT_TEAM`, `XCODE_DEVELOPMENT_TEAM` | Apple team ID for signing WDA (`ios.wda.developmentTeam` in `.swipium/config.json` takes precedence). |
| `APPIUM_HOME` | Extra location searched for an Appium-installed WDA (besides `~/.appium` and global npm). |
| `WDA_PROJECT_PATH`, `WEBDRIVERAGENT_PROJECT` | Extra `WebDriverAgent.xcodeproj` candidates reported by `qa_doctor` and `qa_wda` status (to build one, pass `wdaProjectPath`). |
| `SWIPIUM_ALLOW_REMOTE_WDA` | Comma-separated exact non-loopback WDA base URLs you pre-approve. Set it in your client config, never in the repository. |
| `SWIPIUM_TEST_*` | Test-account values: `_EMAIL`, `_USERNAME`, `_PASSWORD`, `_OTP`, `_TOKEN`, `_PIN` (and `_DEEP_LINK` for the starter flows). Flows and `qa_act` use `${SWIPIUM_TEST_EMAIL}`; `fixtures.json` uses `"var": "SWIPIUM_TEST_EMAIL"`. Child processes Swipium starts (builds, Metro, xcodebuild, adb, OCR and mask commands) don't inherit them; fixture seed scripts do. |
| Other `SWIPIUM_*` | Flows and fixtures read **only** `SWIPIUM_*` names from the environment (never `${HOME}` or `${AWS_SECRET_ACCESS_KEY}`). Names with a credential part (`password`, `secret`, `token`, `otp`, `pin`, `cvv`, `key`, `code` and similar, matched per name part, so `SWIPIUM_SHIPPING_ADDRESS` is not one; [rule](docs/flows.md#variables)) are secrets and redacted. Child processes other than seed scripts don't inherit `SWIPIUM_SECRET_*` or any of these secret names. |
| `SWIPIUM_OCR_CMD` | OCR command for `qa_visual` `find_text` (none bundled; consent-gated). `{image}` becomes a PNG path; it prints `[{"text","confidence","bbox"}]` JSON. `ocrCommand` in `config.json` wins. |
| `SWIPIUM_VISUAL_MASK_CMD` | Masks screenshots before OCR and visual providers see them (`visualMaskCommand` in config wins). |
| `SWIPIUM_REQUIRE_ELICITATION=1` | Refuse every consent-gated action (`CONSENT_REFUSED`) when the client can't show a real consent prompt. Operator pre-approval still applies. |
| `SWIPIUM_CONSENT_PREAPPROVE` | Comma-separated exact consent action names approved without a prompt (for example `prepare_plan,test_this_plan`), for headless clients such as `codex exec` or `claude -p` that answer prompts automatically. No wildcards; unknown names are ignored with a warning. Actions that run code also need `SWIPIUM_CONSENT_PREAPPROVE_RUN_CODE=1`, and `wda_non_loopback` is never accepted (use `SWIPIUM_ALLOW_REMOTE_WDA`). Read only from the server process env, never the repository. Each approval is still single-use and is logged at `warn` with the exact command. See [Operator pre-approval](docs/concepts.md#operator-pre-approval). |
| `SWIPIUM_CONSENT_PREAPPROVE_RUN_CODE` | `1` lets `SWIPIUM_CONSENT_PREAPPROVE` cover actions that run repository- or model-chosen code (`build_from_source`, `flow_mutation_run`, `ocr_run`, `start_metro`, `suite_fresh_state_replay`, `wda_build`, `wda_start`, and a `test_this_plan` that includes a build). Under `codex exec` that code runs outside the client's sandbox. Trusted repositories only. |
| `SWIPIUM_RETENTION_DAYS` | Age limit in days for `~/.swipium/runs` session directories (default 30). `0` or `off` disables the automatic prune; `swipium gc` still works. |
| `SWIPIUM_RETENTION_KEEP` | Number of newest sessions per project kept regardless of age (default 20), while the project's root still exists. Sessions of deleted roots, such as temporary directories, age out by `SWIPIUM_RETENTION_DAYS` alone. |
| `SWIPIUM_MAX_WAIT_MS` | How long one `qa_job_status` long-poll, `qa_wait`, or `qa_test_this {waitForCompletion:true}` may block, in ms (default 50000, clamped to 1000 to 230000). Set it just under your client's tool timeout; `swipium init claude` writes 110000. See [Server settings](docs/mcp-server.md#server-settings). |
| `SWIPIUM_TOOLS` | `core` lists only the core tools (the `swipium init codex` set); `all` (default) lists every tool. Tool counts then count the listed tools. |
| `SWIPIUM_DEVICE` | Pins the server to one device: an adb serial, AVD name, or simulator UDID or exact name (case-insensitive in every tool; a simulator name several simulators share is refused with `MULTIPLE_DEVICES`, so pin by UDID). Runs and auto-binding use only that device, a call naming another device (low-level `qa_ios`, `qa_wda` and `qa_bundletool` actions included) is refused with `DEVICE_PINNED`, and `SWIPIUM_CONSENT_PREAPPROVE` therefore only applies to it. Set it on shared CI runners. Read only from the server env ([Device pin](docs/concepts.md#device-pin)). |
| `SWIPIUM_ANDROID_DUMPER` | `agent` turns on the opt-in persistent Android UI dumper (about 2x to 7x faster snapshots, depending on host load; pushes a small jar to the emulator; blocks other UiAutomation clients while it runs). See [android-agent/README.md](android-agent/README.md). |
| `SWIPIUM_EMULATOR_GPU` | `-gpu` mode for emulators Swipium boots (default `auto`). Use `swiftshader_indirect` on hosts without a usable GPU. |
| `SWIPIUM_EMULATOR_COLD_BOOT` | `1` boots emulators cold (`-no-snapshot-load`) instead of quick-booting from the AVD snapshot. |
| `SWIPIUM_LOG_LEVEL` | Minimum stderr log level: `debug`, `info` (default), `warn`, or `error`. `debug` adds one line per tool call (tool, session, duration, error code; never argument values). |
| `JAVA_HOME` | JDK used by Gradle builds; `qa_doctor` flags a missing JDK under Codex. |
| `CODEX_HOME` | Where `swipium init codex` finds `config.toml` (default `~/.codex`). |
| `CLAUDE_CONFIG_DIR` | Where `swipium init claude --scope user` installs the skill (`<dir>/skills/`, default `~/.claude`). |
| `CI` | When set, reports label the run environment as CI. |
| `GITHUB_SHA`, `GITHUB_REF_NAME`, `GITHUB_SERVER_URL`, `GITHUB_REPOSITORY`, `GITHUB_RUN_ID`, `CI_COMMIT_SHA`, `CI_COMMIT_REF_NAME`, `CI_PIPELINE_URL`, `BITBUCKET_COMMIT`, `BITBUCKET_BRANCH` | Source revision (commit, branch, run URL) recorded in reports and the issue ledger. |
| `SWIPIUM_DISABLE_DEVICE_DISCOVERY` | Test-suite isolation only: disables device auto-discovery. Not for normal use. |
An invalid `SWIPIUM_MAX_WAIT_MS`, `SWIPIUM_TOOLS`, or `SWIPIUM_LOG_LEVEL` falls back to its default with a `warn` line on stderr at startup.
Generated flows, suites, and code never contain credential values; they reference `SWIPIUM_TEST_*`, `SWIPIUM_SECRET_N`, or `SWIPIUM_GEN_<FIELD>`, which you set when replaying. Generated Appium projects read their own variables (`SWIPIUM_PLATFORM`, `APPIUM_HOST`, `ANDROID_*`, `IOS_*`, …), documented in their README.
## CLI reference
With no subcommand, `swipium` runs the stdio MCP server (what clients launch). `swipium --help` prints this list.
| Command | What it does |
| --- | --- |
| `swipium` (alias `swipium serve`) | Start the stdio MCP server. Unrecognized flags alone (such as `--stdio`) are ignored with a warning; an unknown subcommand prints usage and exits 2, and so do flags placed before a subcommand (`swipium -y init claude` prints the corrected command). |
| `swipium init <client> [--apply] [--scope local\|user\|project] [--cwd <dir>] [--no-skill] [--trust-local]` | Preview (default) or apply the registration for `claude`, `codex`, `gemini`, `cursor`, or `vscode`. `claude` and `codex` also install the swipium-qa skill (`--no-skill` skips it); `--trust-local` (claude, local or user scope) pre-approves the consent actions that run no code. `--flag value` and `--flag=value` both work. Exits 2 on an unknown or incomplete option, a missing `--cwd`, or a config file it won't edit, and 1 when the apply fails or `claude` already has a different registration (it prints the remove and add commands). See [docs/mcp-server.md](docs/mcp-server.md). |
| `swipium init flows [--root <dir>] [--force]` | Write starter flow templates into `.swipium/flows/` (existing files kept unless `--force`). A `--root` that doesn't exist exits 2, and so do the registration flags (`--scope`, `--apply`, `--cwd`, `--no-skill`, `--trust-local`). |
| `swipium verify` | Start the server over stdio, check that every tool is listed, and run `qa_doctor`, listing every failed check. Exits 1 when the server or a tool is missing, `qa_doctor` errors, or Node.js is too old; `--help` prints usage. |
| `swipium scan [path] [--check \| --dry-run \| --no-write]` | Print a readiness report: `READY`, `PARTIAL` (for example no installable build yet, but `qa_test_this` will build one from source), or `BLOCKED`. Creates `.swipium/` and merges its results into `.swipium/config.json` (your keys are kept) unless the result is `BLOCKED` or a no-write flag is set. Exits 0 on `READY` or `PARTIAL`, 1 on `BLOCKED`, 2 on a bad path or option. |
| `swipium suite <lint\|compile\|init> [projectRoot] [--suite <file>] [--strict] [--force]` | `lint` flags brittle page-object locators, unresolved references, and coordinate-only steps (exits 0 clean or with warnings only, 1 on errors or, with `--strict`, any finding, 2 with no `.swipium/pages`). `compile` turns a POM suite into flows under `.swipium/flows/`, keeping a flow you edited by hand (new output in `<slug>.yaml.new`; `--force` overwrites; writing a flow removes a leftover `.yaml.new`); it exits 0 when every test compiled, else 2. `init` explains suites. Step numbers in messages start at 1. An option the subcommand doesn't take (`lint` takes `--strict`, `compile` takes `--suite` and `--force`, `init` none), including a typo such as `--stirct`, prints usage and exits 2, as do an unknown subcommand (with a "did you mean" hint for a one-letter typo) and `--suite` without a value. `--help` prints usage and exits 0. The file layout is in [docs/flows.md](docs/flows.md#pom-suites). |
| `swipium report --format html\|junit\|sarif\|github-summary\|markdown\|json` | Render a saved report (`--latest` default, `--session`, `--report`, `--root`, `--out`, `--max-age <minutes>`, `--fail-on-gate`). Exits 1 when the gate blocks with `--fail-on-gate`, 2 on usage error (`--fail-on-gate=false` included), no report, a report older than `--max-age`, newer sessions without a report under `--latest --fail-on-gate` (without `--max-age`), a failed `--out` write, or an invalid `.swipium/policy.json`. |
| `swipium gc [--dry-run] [--days N] [--keep N]` | Delete old `~/.swipium/runs` session directories and stale `~/.swipium/projects.json` entries. The newest `--keep` sessions are kept only for projects whose root still exists. `--days` and `--keep` take a non-negative integer; an empty (`--days=`) or non-integer value exits 2 and deletes nothing. Exits 1 when some entries could not be processed. |
| `swipium --help` / `-h`, `--version` / `-v` | Print usage or the version. |
## CI
`swipium report` renders a finished run as a self-contained HTML report, JUnit, a GitHub job summary, Markdown, JSON, or (experimental) SARIF; `--fail-on-gate` fails the job when the `.swipium/policy.json` release gate blocks. The CLI does not drive devices: CI still needs an agent (such as `claude -p`) calling the MCP tools, including `qa_flow_run` to replay saved flows. Nobody answers consent prompts in CI, so either install the app in the workflow before the agent step (safest) or [pre-approve](docs/ci-reports.md#pre-approving-consents-in-ci) the exact actions the agent needs. Recipe: **[docs/ci-reports.md](docs/ci-reports.md)**.
## Upgrading
Restart your MCP client after every upgrade; a client still running the old server gets `STALE_CLIENT` errors.
- **From 2.3.x to 2.4.0:** JUnit marks tool and environment failures and the not-tested case as `<error>` and lists only high findings; the latest attempt per workflow wins in the release gate (two separate `qa_flow_run` calls that fail, then pass, now pass as flaky, and scripts counting `testOutcomes` in `report.json` must skip `superseded` entries); `report.md` is at most 2 KB, so parse `report.json`; SARIF is experimental and only holds high app findings; the Playwright export is deprecated; the CI recipe uploads `swipium-report.html` instead of `~/.swipium/runs`. See the [2.4.0 upgrade checklist](CHANGELOG.md#240---2026-11-01).
- **From 2.2.x to 2.3.0:** Node.js 22.12 or newer; `qa_test_this` and `qa_test_feature` run by default (`mode:"plan"` previews); element lines drop empty names and mark non-clickable elements with a trailing ` -`; `qa_report` returns a lean view (`format:"json"` for the full report); a run that tested nothing (including one that launched the app but exercised no workflow, or only has a `qa_note` pass with no device evidence) is never SHIP and fails `--fail-on-gate`; budget stops are `ok:false` (`BUDGET_EXHAUSTED`) and the default budget is 30 min / 60 actions / 40 screenshots; `qa_act type` (and flow `inputText` with `into`) fails with `TEXT_INPUT_MISMATCH` when the field doesn't read back what was typed, coordinate targets included; an app that renders nothing, or stays on its loading screen, at launch is `blocked` with `BLANK_SCREEN`; shared folders such as `~/Library`, `~/Downloads`, `/tmp` or `/Applications` are refused as `projectRoot`; managed WebDriverAgents run on a per-simulator port (8101 to 8900) instead of 8100; an invalid `.swipium/policy.json` makes `swipium report` exit 2; Swipium-booted emulators quick-boot from their snapshot with `-gpu auto`. See the [2.3.0 upgrade checklist](CHANGELOG.md#230---2026-10-12).
- **From 2.0.x to 2.2.0:** element lists in `structuredContent` are now `@eN` strings (pass `responseMode:"verbose"` for objects), Codex needs the `env_vars` line, `qa_wda build` returns a job, and waits end within about 50 s. See the [2.2.0 upgrade checklist](CHANGELOG.md#220---2026-10-05).
- **From 1.5.x:** 2.0 removed and renamed tools, reads only `SWIPIUM_*` variables in flows and fixtures, and asks consent for every install. Work through the [2.0.0 upgrade checklist](CHANGELOG.md#200---2026-09-30) and the [migration table](CHANGELOG.md#migrating-from-150) first, then the 2.2.0 one.
## Troubleshooting
Every error has a `failureCode`, `nextSteps`, and `retrySafe`. `qa_explain_blocker` explains any code, `qa_doctor` checks the toolchain, and the full catalog is in [docs/tools.md](docs/tools.md#failure-codes).
| `failureCode` | Meaning | What to do |
| --- | --- | --- |
| `PROJECT_ROOT_UNRESOLVED` | Swipium can't tell which app to test. | Name the absolute path in the prompt or set `SWIPIUM_PROJECT_ROOT` ([project root](docs/concepts.md#project-root)). |
| `STALE_CLIENT` | The client runs a pre-upgrade server or uses a removed tool. | Restart the MCP client. |
| `ADB_NOT_FOUND` | Android platform-tools can't be found. | Install them via Android Studio; set `ANDROID_HOME` in the server `env` for GUI clients or non-default locations. |
| `NO_DEVICE` | No emulator is online and none can be booted, the requested device doesn't exist, or the bound device went away. | Create an AVD or iOS Simulator; `qa_test_this` boots it for you. |
| `DEVICE_PINNED` | The call names a device other than the one `SWIPIUM_DEVICE` pins. | Omit `device`, or change `SWIPIUM_DEVICE` in the server env. |
| `PHYSICAL_DEVICE_UNSUPPORTED` | Only a real phone is available. | Start an emulator, or unplug the phone. |
| `METRO_REQUIRED` | A debug RN/Expo build needs Metro serving. | Start Metro (`qa_metro {action:"start"}` or `npx react-native start` / `npx expo start`), then retry. If Metro is already serving and an Expo dev client just isn't connected, open the deep link the blocker gives (or tap the server in the dev client). |
| `METRO_WRONG_PROJECT` | The Metro on port 8081 serves another project. | Stop the other project's Metro, start this project's, then retry. |
| `EXPO_PREBUILD_REQUIRED` | Generating the Expo native project failed during the build. | Run `npx expo prebuild` to see the error, fix it, then retry. |
| `WDA_UNREACHABLE` | WebDriverAgent isn't running or answering (or hung after the launch). | `qa_wda {action:"status"}`, then `start` or `attach`. A hung WDA: `qa_wda {action:"stop"}`, then `setup` and `attach`. For a plain smoke check, use `goal:"smoke"`, which works visual-only. |
| `DEVICE_UNRESPONSIVE` | The Android emulator didn't answer adb in time (stalled or overloaded); an input may have partly landed. | `qa_screenshot` to see the screen, then restart the app or the emulator and retry. |
| `BACKEND_UNSUPPORTED` | The action needs WDA (iOS visual-only mode). | Attach WDA with `qa_wda`, or use `qa_visual` and `qa_screenshot`. |
| `CONSENT_DECLINED` / `CONSENT_CANCELLED` / `CONSENT_REFUSED` | Nothing ran: you declined, the prompt was dismissed, timed out or failed (`elicitationFailure` says why), or `SWIPIUM_REQUIRE_ELICITATION=1` blocked it. | Re-call for a fresh prompt if you want the action. `likelyAutomatic: true` means a headless client answered in under 1.5 s: don't loop, pre-approve the named `action` with `SWIPIUM_CONSENT_PREAPPROVE` ([Headless runs](docs/mcp-server.md#headless-runs)). |
| `NOT_TESTED` | The run never reached or exercised the app (a launch plus an agent's pass note is not a test), so its report fails the release gate. | Fix the blocker the report names, or run `qa_smoke` or a flow, then re-run. |
| `BLANK_SCREEN` | The app showed no UI after launch (about 15 s, up to 30 s while Metro is serving), or was still on its splash or loading screen at the end of the wait, so the launch is not verified. | Check that Metro serves the bundle (`qa_wait {for:"metro_ready"}`) and read the logs (`qa_device_info {logs:{buffer:"crash"}}`). A canvas or game screen with no accessibility nodes looks blank too: check it with `qa_visual`. |
| `TEXT_INPUT_MISMATCH` | After typing, the field holds other text (reordered, characters dropped, or old text left). | Snapshot again and retry the whole value in replace mode (never repeat a failed append); type long text in shorter parts. A max length or input mask that only cuts or reformats the text is a warning, not this code. |
| `BUDGET_EXHAUSTED` | The session's action, screenshot, or time budget is spent; the call did nothing (`ok:false`). | Call `qa_report`, or start a session with a larger `budget`. |
| `CANCELLED` | The call or job was cancelled. Not a failure, never recorded as a finding. | Re-call if you still want it. Cancelling a call doesn't stop a background job; `qa_job_cancel` does. |
| `INVALID_ARGUMENT` | An undeclared, missing, or wrong-typed argument, an unknown `sessionId`, `jobId` or artifact URI, or a tool hidden by `SWIPIUM_TOOLS=core`. Nothing ran. | Fix the argument; the result lists `acceptedParameters`. Over-large `waitMs` / `timeoutMs` values are clamped to the cap (50000 by default) with a note, not rejected. |
| `STALE_REF` | The `@eN` ref's element is no longer on screen. | Take a new `qa_snapshot`, or use one of the `candidates` in the error. |
If tool calls time out in the client, raise its tool timeout (Codex `tool_timeout_sec = 600`): waits and job polls end within about 50 s, but a few synchronous steps, such as a large `qa_ios install`, long saved flows in `qa_smoke`, or `qa_mobile_audit`, can take longer.
## Security
Swipium is a local stdio process and the server opens no network port. A WebDriverAgent it manages for iOS takes commands on 127.0.0.1 only, but its MJPEG screen stream (port = WDA port + 1000, for example 9101) listens on every network interface, because WebDriverAgent can't restrict it; use a firewall on untrusted networks. Actions with side effects need your [consent](docs/concepts.md#consent); each approval is single-use and bound to its session, and only the server's own environment can pre-approve actions (`SWIPIUM_CONSENT_PREAPPROVE`), never a repository file or the model. Known secret values are [redacted](docs/concepts.md#secrets-and-redaction) from snapshots, artifacts, and reports, and generated output is checked for leaked secrets. A cloned repository's `.swipium/` is treated as untrusted: repo-supplied commands are shown verbatim in the consent prompt, flows read only `SWIPIUM_*` variables, and Swipium never runs `git`. Screenshots are never redacted, so use `qa_start_session { sensitive: true }` to refuse all screen captures when that matters. Details: [Threat Model](THREAT_MODEL.md). Report vulnerabilities privately per the [Security Policy](SECURITY.md).
## Contributing and license
See [CONTRIBUTING.md](CONTRIBUTING.md), [SUPPORT.md](SUPPORT.md), the [docs index](docs/README.md), and the [CHANGELOG](CHANGELOG.md). MIT licensed; see [LICENSE](LICENSE).
TDQS
Scored across 55 tools
Many tools overlap in the high-level orchestration space: qa_test_this, qa_test_feature, qa_mobile_audit, qa_smoke, qa_explore, and qa_prepare_target all 'prepare/run tests on a device.' Descriptions add clarifying notes (e.g. qa_test_this 'start here', qa_start_session for low-level), but an agent must read carefully to choose. Lower-level tools (qa_act, qa_snapshot, qa_inspect, qa_visual) are distinct.
Consistent qa_ prefix and snake_case throughout. Most are verb_noun (qa_start_session, qa_get_artifact, qa_clean_overlay) but several are noun-first (qa_app_map_read, qa_suite_read, qa_job_status, qa_device_info, qa_screen_record), a minor deviation.
55 tools is heavy for a mobile QA server; groups like app-map (5), suite (5), flow (4), and the many orchestrators suggest feature sprawl. Each earns some purpose but the surface is bloated and would benefit from consolidation.
Covers the full lifecycle: device/emulator management, WDA, build/bundletool, prepare, session, snapshot/act, flows, suites, app maps, visual checks, health, reports, job control, blocker recovery. No obvious dead ends in the stated domain.