# averi setup guide > **This document is written for an AI coding agent** setting up averi in a user's app repo. > Human reading this? It works fine as a manual guide too — but the fastest path is to paste > the prompt from the [README](README.md#installation) into your agent and let it drive. You are setting up [averi](https://github.com/mholecy/averi) — an MCP server that lets you verify mobile app changes on iOS Simulators and Android Emulators. Setup means creating three things in the **app repo root**: the MCP registration, an `averi.yaml` describing the app's states and flows, and a gitignored `.env.averi` with test credentials. Follow the steps **in order**. Every step ends with a **Check** — run it and do not move on until it passes. Do not declare setup complete until the final verification in step 6 passes. Shell commands in this guide are POSIX (macOS/Linux/WSL). On native Windows, translate them to PowerShell equivalents (note `curl` there is an alias for `Invoke-WebRequest` — use `curl.exe`). --- ## Step 0 — Prerequisites averi needs Node 20+ and the platform tools for whichever platforms the project targets. Ask the user which platforms they need if the repo doesn't make it obvious. **Android** (macOS/Linux/Windows): - `adb` on PATH (Android SDK platform-tools) - An emulator running **iOS** (macOS only — Apple ships simulators only with Xcode): - Xcode and a booted simulator - `idb`: `brew tap facebook/fb && brew install idb-companion && pipx install fb-idb --python python3.13` (fb-idb breaks on Python 3.14 — pin 3.13) - If `xcode-select -p` points at CommandLineTools, that's fine — averi injects `DEVELOPER_DIR` itself when Xcode is at `/Applications/Xcode.app` (anywhere else, set `DEVELOPER_DIR` in `.mcp.json`'s `env`), no sudo needed. - Only for `app.ios.treeSource: wda` (step 4): averi builds WebDriverAgent with `xcodebuild` from the `appium-webdriveragent` package — an optional dependency installed with averi (`npx -y averi` included). If that optional install was skipped or failed (e.g. `--omit=optional`), `treeSource: wda` fails with a message naming the package. **Check:** ```bash node --version # >= 20 adb devices # Android: lists a device/emulator (state "device") xcrun simctl list devices booted # iOS: lists at least one booted simulator idb list-targets # iOS: runs without error ``` Only verify the platforms the project targets. If a tool is missing, tell the user the exact install command from above — don't skip the platform silently. > **Non-macOS hosts (Linux/Windows):** the full Android toolset works; iOS tools error only > when called. Bake two things into the setup: always pass `platforms: ["android"]` to > `verify` (its default runs **both** platforms, so the iOS leg would error), and know that > OCR-backed checks — `ocr` asserts and the text/type-size parity tables — use the macOS > Vision framework: off-macOS they fall back to the accessibility tree or fail closed. > Windows is **untested** — if something breaks, please report it on GitHub. --- ## Step 1 — Register the MCP server averi is on npm; nothing to clone or build. Two ways to register, same result — ask the user whether this is just for them or for the whole team: **Just this user:** ```bash claude mcp add averi -- npx -y averi # add --scope user to enable averi in all of the user's projects at once ``` **Whole team** — create `.mcp.json` at the repo root and commit it (teammates get a one-time approval prompt). Pin the version so everyone runs the same build — `` is what `npm view averi version` prints: ```json { "mcpServers": { "averi": { "command": "npx", "args": ["-y", "averi@"] } } } ``` > **Native Windows:** `npx` is `npx.cmd`, which MCP clients cannot spawn directly — wrap it in > `cmd /c`: register with `claude mcp add averi -- cmd /c npx -y averi`, or in `.mcp.json` use > `{ "command": "cmd", "args": ["/c", "npx", "-y", "averi"] }`. WSL needs no wrapper. Either way the agent session runs the server with the **repo root as its working directory** — that's how averi finds `averi.yaml`. No paths need configuring in averi itself. > **Monorepo / nested repo?** If sessions run somewhere other than the directory holding > `averi.yaml`, the tools that read averi.yaml accept `configPath: app/averi.yaml`. Everything the config points at > (build paths, `.env.averi`, `.averi/baselines/`) resolves against **the config file**, not the > working directory, so the yaml stays identical either way. **Check:** the session must be restarted for the tools to appear — tell the user, and after restart confirm you have 18 `averi` tools (`list_devices`, `select_device`, `install_app`, `launch_app`, `terminate_app`, `open_deep_link`, `screenshot`, `ui_snapshot`, `tap`, `swipe`, `scroll_until`, `type_text`, `press_key`, `ensure_state`, `run_flow`, `assert`, `verify`, `get_logs`). Then call `list_devices` — it should list the booted device(s) from step 0. > With a phone, an emulator, and a watch emulator all connected, pin the right one with > `select_device` — otherwise tools target the first booted device per platform, an arbitrary > pick. A pinned device going offline is an error, never a silent fallback. --- ## Step 2 — Credentials: `.env.averi` Credential **values** never live in yaml — `averi.yaml` references them as `${ENV_VAR}` and the values come from a gitignored `.env.averi` next to it (auto-loaded; real env vars take precedence, so CI injects secrets normally). Values are redacted (`***`) from every trace and error — you will never see them. `.env.averi` supplies `${VAR}` credential values and `AVERI_ENV` only. It is read into the run's own environment on every tool call and is never exported to the shell or to the child processes averi starts (adb, xcrun, xcodebuild) — toolchain variables such as `ANDROID_SERIAL`, `ANDROID_HOME` or `DEVELOPER_DIR` belong in the shell or in `.mcp.json`'s `env`, not here. 1. Add `.env.averi` to `.gitignore` **before** creating the file. 2. Ask the user for the test credentials their login flow needs and write them to `.env.averi` at the repo root. Variable names are free-form — they only have to match the `${...}` references you'll write in `averi.yaml`: ``` APP_USERNAME=... APP_PASSWORD=... ``` **Never ask the user to paste credentials into the chat if they'd rather not** — they can edit `.env.averi` themselves; you only need to know the variable *names*. **Check:** ```bash git check-ignore .env.averi # must print the path — if not, fix .gitignore first ``` --- ## Step 3 — Install the agent skill Copy the skill so future sessions know the golden path (build → install → `ensure_state` → navigate → assert → `verify`) without re-deriving it: The skill is a directory: `SKILL.md` plus `references/`, which it links to. Copy the whole directory from the published package, the same version the MCP server runs (`averi@` when pinned): ```bash mkdir -p .claude/skills/averi curl -fsSL "$(npm view averi dist.tarball)" \ | tar -xz -C .claude/skills/averi --strip-components=2 package/skill ``` (For agents other than Claude Code, put the directory wherever that agent loads skills/instructions from.) **Check:** `.claude/skills/averi/SKILL.md` exists and its YAML frontmatter has `name: averi`, and `.claude/skills/averi/references/` holds `averi-yaml.md`, `tools.md` and `troubleshooting.md`. --- ## Step 4 — Author `averi.yaml` by driving the app **Do not hand-author the login flow — bootstrap it by driving the real app.** You need a booted device and the dev build installed (the user's normal build produces the `.apk`/`.app`; averi never builds the app — it installs what the yaml points at). Procedure: 1. Ask the user for the build output paths (`.apk` / `.app`) and package/bundle ids, or find them in the build config. Start `averi.yaml` from this skeleton: ```yaml app: # activity is optional but SET IT if the debug build bundles LeakCanary: without it, # launches resolve the launcher activity via monkey, which picks ARBITRARILY among the # package's launcher activities — and LeakCanary registers one. android: { package: com.example.dev, apk: app/build/outputs/apk/dev/debug/app.apk, activity: .MainActivity } ios: { bundleId: com.example.dev, app: build/Debug-iphonesimulator/Example.app } credentials: # env refs only — values come from .env.averi / real env username: ${APP_USERNAME} password: ${APP_PASSWORD} ``` Build paths are relative to the yaml file itself. 2. `install_app`, then launch with `clearState: true` and walk the full login using the test credentials (reference them as `${VARS}` — never paste values into yaml). 3. At each screen use `ui_snapshot` (and `screenshot` when unsure) to find **stable selectors**: prefer `id:`, else exact visible `text:` (text selectors are locale-sensitive — pin the device language they were captured in). Record every screen as flow steps; wrap dismissable interstitials (permission dialogs, promos) in `optional:`. 4. Define two states: - `logged_out` — `detect:` on a stable element of the first screen after `clearState` - `logged_in` — `detect:` on a stable element of the home screen, with `reach: [login]` ```yaml states: logged_out: detect: { element: { text: "Welcome!" } } logged_in: detect: { element: { text: "Accounts" } } reach: [login] flows: login: steps: - launch: { clearState: true } - wait: { element: { text: "Welcome!" }, timeout: 15s } - tap: { text: "Log in" } - tap: { role: textfield } - type: { value: $password } - tap: { text: "Continue" } - optional: - android: { tap: { id: permission_allow_button } } # Android 13+ notifications - wait: { state: logged_in, timeout: 20s } ``` Real-world quirks the schema already covers — reach for these instead of workarounds: per-platform steps (`android:`/`ios:` on one step), keypads whose digits have no resource-ids (`type_pin` with `text_pattern: "{digit}"`), auto-advancing OTP boxes (per-digit typing built in), and `branch:` for state-dependent paths (e.g. Keychain-surviving PIN login on iOS). Full step reference is in the skill you installed in step 3. > **React Native app on iOS:** if `id:` selectors miss static text and containers under the > default tree source, that's expected — add `app.ios.treeSource: wda` to `averi.yaml` (one > line; taps/typing stay on idb, only the tree read routes through WebDriverAgent; first WDA > build per Xcode version takes minutes; it needs the optional `appium-webdriveragent` package, > see step 0). Details: https://github.com/mholecy/averi/blob/main/docs/plans/ios-wda-tree-source.md. > With `wda` averi also sees the iOS soft keyboard: a tap on an element it covers is refused > rather than sent into the keys — unless `app.ios.keyboardDismiss` names what hides it, e.g. > `[{ tap: { id: login_title } }, { accessory: true }]` (a neutral element whose tap does not > submit — a title or label, never a button — or the Done toolbar above a number pad); then > averi taps that first. No key hides the iOS keyboard without submitting, so there is no > default, and `fill: { …, dismissKeyboard: true }` on iOS no longer presses `enter` (it > submitted the form): it does nothing when the fill has already parked the keyboard (the usual > case — typing through the hardware keyboard parks it), taps the configured dismissal when the > keyboard is still up, and otherwise leaves it up with a `⚠` line. A flow that relied on that > `enter` to submit (a search field) taps the app's own > submit/search control instead. The keyboard's return key (e.g. `tap: { label: search }`) works > only while the software keyboard is visibly up (`ui_snapshot` shows the `keyboard` band): after > a `fill` it is usually parked below the screen, where its keys are still in the tree. > **Screens without stable ids?** Recommend to the user that new screens get test identifiers — > `Modifier.testTag("login_submit")` on Android Compose (plus `testTagsAsResourceId = true` on > the semantics root) and `.accessibilityIdentifier("login_submit")` on iOS. Text selectors > break on locale changes; ids don't. Don't block setup on it — flag it in the final report. **Check:** `ensure_state("logged_in")` — it should run the login flow and confirm the state. Iterate on the yaml until it does. The yaml is code: it lives in the repo and gets fixed like code when navigation changes. --- ## Step 5 — Multiple backends (only if the app targets several) Skip this step unless the user says the app points at different backends (dev/staging/…). When credentials differ per backend — usually just the login name — declare it explicitly rather than swapping env vars by hand. A wrong login name is rejected one screen *after* it is typed, so an environment mix-up looks exactly like a credentials problem: ```yaml credentials: # base: shared by every environment username: ${APP_USERNAME} password: ${APP_PASSWORD} environments: # per-backend overrides, layered ON TOP of `credentials` dev: credentials: username: ${DEV_USERNAME} staging: credentials: username: ${STAGING_USERNAME} defaultEnvironment: dev # optional ``` Selection, most specific first: the tool's `environment` argument → `$AVERI_ENV` (set it in `.env.averi` — switching backend becomes a one-line change in an already-gitignored file) → `defaultEnvironment:` → base `credentials:` alone. Only `ensure_state`, `run_flow` and `verify` take the argument. **Check:** run `ensure_state("logged_in", environment: "")` and confirm the first trace line names the active environment and the overridden keys. --- ## Step 6 — Final verification (required) Prove the setup end to end. **Both** runs must pass: 1. **Cold:** launch with `clearState: true` (or run the login flow's launch step), then `ensure_state("logged_in")` — must execute the full login flow and confirm. 2. **Warm:** call `ensure_state("logged_in")` again immediately — must detect the state in ~1 second without re-running the flow (it's idempotent). Then report to the user: - [ ] MCP server registered (personal or team `.mcp.json`, version pinned for teams) - [ ] `.env.averi` created and confirmed gitignored; which variable names it defines - [ ] Skill installed at `.claude/skills/averi/` (`SKILL.md` + `references/`) - [ ] `averi.yaml` committed-ready: which states and flows it defines - [ ] Cold + warm `ensure_state` both passed (include the timings) - [ ] If `treeSource: wda` is enabled: `ui_snapshot` on iOS returned a tree (proves WDA built and started) - [ ] Anything flagged: missing test ids, `treeSource: wda` enabled, platforms skipped and why If any box is unchecked, setup is **not done** — say so plainly and list what's missing. --- ## Updating averi For when the user asks to update averi in an already-set-up repo. An update touches up to three things — the server version, the copied skill, and `averi.yaml` — and only the first one happens by itself. 1. **Find the versions.** Latest published: `npm view averi version`. Currently used: the pin in `.mcp.json`, if the team setup is in place; an unpinned personal registration (`npx -y averi`) resolves the latest version each time the session starts. 2. **Update the server registration.** - *Unpinned personal:* nothing to edit — restarting the agent session picks up the latest. - *Team `.mcp.json`:* bump the pinned version and commit; teammates get it on their next session start. 3. **Re-sync the skill copy — it does not update itself.** The directory `.claude/skills/averi/` was copied once at setup. Replace it with the copy shipped inside the exact version now in use (removing it first drops files a newer version no longer has): ```bash rm -rf .claude/skills/averi && mkdir -p .claude/skills/averi curl -fsSL "$(npm view averi@ dist.tarball)" \ | tar -xz -C .claude/skills/averi --strip-components=2 package/skill ``` Commit it alongside the `.mcp.json` bump so the skill and server never drift apart. 4. **Check for `averi.yaml` schema changes** in the release notes / commit log (https://github.com/mholecy/averi/commits/main) between the old and new version. If a flow step or state field changed, update `averi.yaml` accordingly. 5. **Re-verify.** Restart the session, confirm the `averi` tools are present, and re-run the step 6 proof: cold + warm `ensure_state("logged_in")`. Setup isn't updated until both pass. 6. **Baselines.** If the update legitimately changes rendering or screenshot behavior, expect baseline diffs — delete the affected files under `.averi/baselines/` to re-baseline, and say so in the report rather than silently regenerating everything. --- ## Troubleshooting - **`idb` install fails** — fb-idb requires Python ≤ 3.13: `pipx install fb-idb --python python3.13`. - **An iOS tree read or input behaves differently than `idb ui describe-all --json` / `idb ui tap …` run by hand, or a step fails with `the idb helper exited during this input; it may or may not have reached the simulator`** — averi reads the iOS tree and sends taps, swipes, text and keys through a persistent idb helper (one Python process per simulator, running idb's own CLI; it falls back to the `idb` CLI by itself when it cannot start or dies — but an input it died under is reported, not repeated: look at the screen before you repeat the step). To rule it out, set `AVERI_IDB_HELPER=off` in the environment the MCP host starts the server with (an `env` block in its server entry) and reconnect: every idb call is then the `idb` CLI. - **iOS screenshots, color/OCR asserts or `ensure_state` fail with `idb screenshot - --udid …`, or an iOS screenshot baseline fails where it passed before** — averi captures the iOS screen with `idb screenshot` (through the same helper, or the `idb` CLI), on either tree source. It does not switch to simctl by itself when idb fails. Check that `idb list-targets` lists the simulator as Booted with its companion; to capture through simctl instead, set `AVERI_IOS_SCREENSHOT=simctl` in the environment the MCP host starts the server with (an `env` block in its server entry) and reconnect. idb's capture draws the Dynamic Island where simctl's shows the app: an iOS baseline diff leaves the status band out of both images for that reason (`status band excluded` in its detail), so a baseline stored before still matches. - **Launch opens LeakCanary instead of the app** — set `app.android.activity` (see step 4 skeleton). - **iOS `id:` selectors find nothing on a React Native app, or a `wait:` on a SwiftUI container id (`.accessibilityElement(children: .contain)`) times out while the screen is showing** — idb never exposes container identifiers: `app.ios.treeSource: wda` (step 4), or wait on a button/row id. - **iOS: `idb returned an empty accessibility tree`, or `ensure_state` stops with `Refused to run reach flow …` and a `⛔ reach` line** — idb returns a 0×0 tree on a rendered screen (measured 2026-10-06/07). averi treats it as an unread device: waits and asserts fail instead of reading "absent", and the reach ladder will not run a destructive rung (`clearState`, `destructive: true`) on it, so nothing was wiped. The measured trigger is an earlier WebDriverAgent session on that simulator (`treeSource: wda` in any project, or another XCTest driver): every app process launched after it starts stuck, and waiting does not end it (18+ min measured). The same 0×0 shape is also a healthy app's first read right after a launch (0.4 s after `launch_app`, measured 2026-10-08): `ui_snapshot` reads once more a second later before failing, and its error then says `The read was retried once after 1 s and was still empty` (a healthy launch had a tree by then); waits and asserts poll past it. averi re-enables the simulator's accessibility automation before each launch it makes, but a process that is ALREADY stuck stays stuck: `terminate_app`, then `launch_app` (a `launch_app` on the running app keeps the same process). If that does not clear it, reboot the simulator (`xcrun simctl shutdown && xcrun simctl boot ` — the error spells it with the simulator's UDID), or set `app.ios.treeSource: wda` (step 4) to read through WebDriverAgent. - **iOS: `idb cannot read the UI tree: the simulator's accessibility service named no frontmost application`** — every tree read fails this way from the first call after a simulator boot. An accessibility query made too soon after the boot wedges the simulator's accessibility service until the next reboot (measured 2026-10-31 on nine reboots: a companion started right after `simctl boot` returned wedged it 2 of 3 times, right after `simctl bootstatus -b` 3 of 3, three seconds after bootstatus 0 of 3). Relaunching the app, a new idb_companion and waiting cured it 0 of 5 times each, so averi does not retry the read: a wait, assert, tap or `scroll_until` fails on its first read, and `ensure_state` stops without escalating its ladder. Reboot the simulator (`xcrun simctl shutdown && xcrun simctl boot ` — the error spells it with the simulator's UDID), wait for `xcrun simctl bootstatus -b`, and start idb_companion a few seconds after that; or set `app.ios.treeSource: wda` (step 4) to read through WebDriverAgent. - **`ensure_state` stops with `Refused to run reach flow …` and `every UI tree read was bare` (either platform)** — every tree the detect probe read held nothing of the app's (only wrappers, unlabeled decoration, or iOS's status bar and launch icon): a cold launch's Android decor (`android:id/content`, `action_bar_root`; measured +5.3…+13.8 s after launch on an RN debug build) or the WDA splash (7 nodes, `SplashScreenLogo`). Before a destructive rung (`clearState`, or a flow marked `destructive: true`) — and since 2026-10-08 before any rung when the screen read bare — averi takes a second look of up to 20 s: it ends at once on the state, runs the rung (after the full 20 s) when it saw a rendered screen outside the state, runs a cheap rung anyway on a still-bare screen, and refuses a destructive one rather than wipe an app whose screen it never saw (until 2026-10-07 it read the decor as "not in state" and wiped). Take a `screenshot`: still launching → retry the call; rendered but made only of unlabeled icons (they read as bare too) → `run_flow` the rung deliberately, or give the screen a labelled element to detect on. - **iOS typing lands the wrong characters (`y`↔`z`, `ý` for a digit), or `fill` on a PLAIN field reports fewer characters than typed** — HID typing follows the simulator's hardware keyboard layout, which follows the Mac's input source unless pinned (measured 2026-09-17 with a Slovak host layout; it read as an averi typing bug for 40 minutes). Pin it once per simulator and reboot it: `xcrun simctl spawn defaults write -g AppleKeyboards -array "en_US@sw=QWERTY;hw=US"`. - **WDA refuses to start** — a simulator runs one XCTest UI-test session at a time; another process's WDA is refused loudly. Recovery: `pkill -f WebDriverAgentRunner` or reboot the simulator. - **Right after an upgrade or an `/mcp` reconnect: `A WebDriverAgent answers /status on port 8100, but this session did not start it`** — the previous server was still busy (a tool call in flight) when the host closed it, so it missed the stdin close and took the host's SIGTERM, and before 0.8.1 nothing then stopped its WebDriverAgent; the new server (correctly) refuses to adopt a server it cannot prove serves this simulator. From 0.8.1 the server stops its WebDriverAgent on SIGTERM/SIGINT/SIGHUP, waits for the port to go quiet and kills the listener if xcodebuild's teardown did not. An older server, a SIGKILL, or a second signal during that wait still leave one behind: `pkill -f WebDriverAgentRunner` before the first iOS call. - **A `${VAR}` is missing or empty** — the error names the credential (and environment) that needed it; add the variable to `.env.averi`. If it IS in `.env.averi` and the error says "set but empty", an empty variable exported in the shell or CI shadows it (exported variables win): unset it there. Never ask the user for the value itself. A credential declared as `""` in averi.yaml is refused the same way (`Credential "$password" is empty`, naming the `credentials:` or `environments..credentials` layer it was declared in): a secret is never empty, because typing "" is a no-op and the step would pass with nothing typed. A plain step value `""` (`fill` with `clear: true` to clear a field, MCP `type_text ""`) is not a secret and still works. - **`type_pin "…" has no digits`** — `type_pin` types only the digits of its value (`111-111-111` is formatting); a value with none would type nothing, so it is refused before the first key. Fix the value or the credential. - **Flow times out after a UI change** — fix the descriptor as part of the change; it's code.