{"id":"21fbf7f2-ea79-410a-8efa-02488568edec","slug":"mobium","kind":"mcp","name":"mobium","summary":"Mobile app automation for AI agents and people: Android emulators and phones, iOS simulators and iPhones, and Fire TV, driven from one Go binary through a CLI, an MCP server and five clients.","owner":"mobiumdev","repoUrl":"https://github.com/mobiumdev/mobium","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":2,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-09-26T20:17:04.000Z","addedAt":"2026-10-21T05:13:53.246Z","logoUrl":"https://avatars.githubusercontent.com/u/334318419?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools","cloud-infra","memory-knowledge"],"tags":["agentic-ai","ai-agents","android","android-emulator","cli","e2e-testing","golang","ios"],"runtime":"go","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-21T05:18:22.935Z","sample":false,"url":"https://conduits.dev/tools/mobium","language":"Go","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<img src=\"assets/branding/mobium-logo.png\" alt=\"Mobium\" width=\"140\" align=\"right\">\n\n# Mobium\n\n*Mutatis mutandis.*\n\n**Mobile app automation for AI agents and people.** Android emulators, Android\nphones, iOS simulators and iPhones, driven through one tool layer from a single\nGo binary with no runtime dependencies.\n\n```sh\nmobium launch com.example.shop && mobium map && mobium tap @e5 && mobium map\n```\n\nMobium drives mobile apps the way [Vibium](https://github.com/VibiumDev/vibium)\ndrives browsers: a `map` → `@ref` → act loop that an agent can follow without\nlearning a new model. It is Vibium's architecture with the browser swapped for\na device.\n\n**What it is for:** letting a coding agent check its own work on mobile. An\nagent that changed a login screen should be able to open the app, read what is\nactually on screen, tap through the flow and look at the result — without a\nperson driving an emulator for it. People get the same commands.\n\n- [Quick start](#quick-start)\n- [How it works](#how-it-works)\n- [Platforms](#platforms)\n- [App types](#app-types)\n- [Hybrid apps and WebViews](#hybrid-apps-and-webviews)\n- [Front doors](#front-doors)\n- [Drivers for other platforms](#drivers-for-other-platforms)\n- [Documentation](#documentation)\n- [License](#license)\n\n## Quick start\n\nRequirements:\n\n- **Go 1.24+**, to install it — prebuilt binaries arrive with the first\n  release ([SETUP.md](docs/SETUP.md#installing-a-release))\n- **Android:** platform-tools on `PATH` (or `ANDROID_HOME`, or\n  `MOBIUM_ADB_PATH`), and an emulator or a phone with USB debugging on\n- **iOS:** full Xcode and a booted simulator — or an iPhone on a cable, with\n  an Apple ID (a free one works) added to Xcode\n\n```sh\ngo install github.com/mobiumdev/mobium/cmd/mobium@latest\nmobium doctor       # checks the environment; needs no device and no daemon\n```\n\n```\n$ mobium devices\nemulator-5554    device     (emulator, model: sdk_gphone64_arm64)\n\n$ mobium map\n@e1 Navigate up (button)\n@e2 Email address (input)\n@e3 password (password)\n@e4 Remember me (checkbox, unchecked)\n@e5 Sign In (button)\n@e6 Forgot password? (link)\n\n$ mobium type @e2 \"someone@example.com\"\n$ mobium tap @e5\ntapped @e5 at (540, 930)\n```\n\n**New here? [docs/quickstart](docs/quickstart/README.md)** walks from nothing\nto a script that starts a session, launches an app, taps, screenshots and\nquits — for the command line and for each client, on Android and iOS, with\nthe output and screenshots of it running. **Contributing?**\n[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) goes from a fresh clone to a change\nthat passes CI.\n\n[docs/SETUP.md](docs/SETUP.md) covers emulators, Android phones over USB and\nWi-Fi, iOS simulators and iPhones, and the setup traps whose error messages\nname the wrong cause. `mobium doctor` checks the same list and names the fix.\n\n## How it works\n\n### Refs\n\n`mobium map` snapshots the accessibility hierarchy, keeps the nodes a person\ncould act on, and gives each one the most durable locator that identifies it\nuniquely — a resource-id if there is one, then an accessibility label, then\nvisible text, and a sibling path only as a last resort.\n\n`mobium tap @e5` **re-snapshots and re-resolves** that locator before touching\nanything. It never replays the coordinates from the map, because the screen\nmoves between commands and a tap at a stale point fails silently. A locator\nthat has become ambiguous is refused rather than guessed:\n\n```\n$ mobium tap text=Continue\nerror: text=Continue matches 2 elements — narrow it with role= or use a @ref from `mobium map`\n```\n\n### One locator vocabulary\n\nThe same locator means the same thing on both platforms, compiled per\nplatform:\n\n| Locator | Android | iOS |\n| --- | --- | --- |\n| `text=Sign In` | `text` | `value`, or the label of a control with no text of its own |\n| `label=Email` | `content-desc` | accessibility label |\n| `testid=submit` | `resource-id` | `accessibilityIdentifier` |\n| `role=button` | `android.widget.Button` and kin | `XCUIElementTypeButton` |\n\nCoordinates are device pixels on both platforms, so a point read off a\nscreenshot can be tapped directly.\n\n### Actions wait, and refuse what they cannot do\n\nBefore acting, every action waits for its target to exist, be in view, stop\nmoving and be enabled, and scrolls to it if it is off screen. It refuses —\nrather than taps — a target under a dialog or the keyboard, and `type` refuses\nwhat is certainly not a text field. When the app itself has drawn a control\nover the target, a tap aims at a clear part of it, waits for the cover to go,\nor refuses and names the cover; anything else drawn over the point is\nreported in the result. Inside a WebView the page itself is asked, with\nVibium's checks: the element is scrolled into view, and a tap waits for it to\nbe visible, enabled, still and not covered — the page's own hit test says\nwhat is on top — and otherwise refuses with the check that failed. `type` there fills a field the way Vibium's\nfill does: only a field that can take text, confirmed by reading it back, a\npassword never echoed. `wait` blocks for an element to appear,\ndisappear or read a certain way, so no flow needs a sleep.\n\nA backend that cannot do something correctly says so and names the fix,\ninstead of approximating it. `adb shell input text` mangles quotes and\nnon-ASCII, so the zero-install backend declines text entry rather than typing\nthe wrong string.\n\n### What it covers\n\n| | |\n| --- | --- |\n| Reading | `map`, and `map --diff` for what an action changed; `text`, `find`, `screenshot`, the raw `source` with passwords hidden, WebView contexts |\n| Acting | tap, double tap, type, swipe, long press, drag and drop, two-finger zoom and rotate; `check`/`uncheck` reach a state and confirm it |\n| Waiting | `wait` for appear, disappear, text, enabled or disabled |\n| Devices | `devices` lists them; `boot` starts an emulator or a simulator and answers once it is up, `shutdown` stops one after ending its session — an agent with no shell can start a device |\n| Apps | launch, terminate, install, uninstall, list, clear data, open a URL or deep link, foreground app, any app's state, backgrounding and resuming |\n| Files | `upload` and `download` between this machine and where the device keeps downloads — the Download folder on Android, an app's Documents on iOS — or, with `--device-path`, a file or folder at any path: a shell path on Android, an app's private data for a debuggable build, an app's data container on iOS |\n| Device state | permissions, appearance, accessibility settings for the session (reduce motion, bold text, contrast, text size and more; a simulator and Android), orientation, per-app language, hardware buttons and a TV remote's D-pad and media keys, shake and biometrics (enroll, then a matching or a stranger's face or finger) on emulators and simulators, screen lock, simulated calls and messages, notifications, timezone and the device's clock, battery, clipboard, geolocation and routes |\n| Dialogs | `alert` reads, answers and types into a system or app dialog; `dialogs` declares answers for one that gets in an action's way |\n| Diagnostics | device logs, crash reports and ANRs, screen recording, a session `trace` in Vibium's record format, for player.vibium.dev, `hit-test` — which view UIKit would really give a tap, below accessibility (iOS; before every tap with `launch --hit-test` on a simulator), `audit` — Apple's own accessibility audit of the screen (iOS), `doctor` |\n| Batches | `batch` runs a known sequence of calls in one, each checked before the first runs, stopping at the first failure |\n\n[docs/API.md](docs/API.md) lists every tool and which front door reaches it,\nand [docs/FLAGS.md](docs/FLAGS.md) every argument. Both are generated, and the\nbuild fails if either is stale. Add `--json` to any command for its structured\nanswer.\n\n**Where the platforms differ, the answer says which question it answered.**\nAndroid reads a mock location back and confirms it; iOS's `simctl` has no\n`get`, so there the answer is that the request was accepted. iOS reads the\nclipboard; Android refuses the read, because since Android 10 only a focused\napp may, and reporting \"empty\" would be a different and usually false claim.\n\n## Platforms\n\nVerified on devices, not only compiled: a Pixel 7 emulator on Android 15 and\nAndroid 17, a Pixel 8 Pro on Android 17, an iPhone 17 Pro simulator on iOS\n26.5, and an iPhone 15 Plus on iOS 26.6.2. The end-to-end flows in\n[docs/checks/](docs/checks/) each drive a real device and assert on what\nhappened rather than on an exit code.\n\n| | `uiautomator2` | `uiautomator` | `wda` |\n| --- | --- | --- | --- |\n| platform | Android (default) | Android | iOS simulator and iPhone |\n| installs on the device | two APKs, once | nothing | one runner, once |\n| `map` on an emulator | 0.04s | 1.96s | — |\n| type into an element | yes | no | yes |\n| two fingers, drag, double tap | yes | no | yes |\n| a screen that never stops moving | fine | cannot read it | fine |\n\n**Android** uses the [UiAutomator2 server](https://github.com/appium/appium-uiautomator2-server)\ndirectly over HTTP, with no Node in between. Pick `--driver uiautomator` when you cannot install anything on the\ndevice; it reads through `uiautomator dump`, which waits for the screen to go\nidle and so cannot read one that animates.\n\n**iOS simulators** use the prebuilt\n[WebDriverAgent](https://github.com/appium/WebDriverAgent) runner, installed\nwith `simctl`. **A real iPhone** takes the same backend: turn on Developer\nMode and Settings > Developer > Enable UI Automation, add an Apple ID to\nXcode, and the first command builds and signs WebDriverAgent for your team.\nControls that only `simctl` has — permissions, appearance, clipboard,\nsimulated location — are refused on a phone with the reason; accessibility\nsettings go through the phone's own Settings app.\n[docs/SETUP.md](docs/SETUP.md#ios-real-device) has the steps.\n\n**TVs** are driven with a remote: a Fire TV over `adb connect`, an Android\nTV, and since 2026-10-09 an Apple TV simulator, which takes WebDriverAgent's\ntvOS runner. `press` sends the D-pad, select, back and media keys, and a\nD-pad press is reported by where focus went. An Apple TV has no touch screen,\nso a tap there is refused, naming the remote\n([docs/SETUP.md](docs/SETUP.md#an-apple-tv-simulator)).\n\n**Wear OS** is Android, and the Android backends drive it with nothing\nadded: since 2026-10-13 on a Wear OS 5.1 emulator, through Wear's own\nSettings — its chips, its toggles and its own dialog, which `app_alert`\nanswers — with swiping right as a watch's back\n([docs/checks/wear-os.sh](docs/checks/wear-os.sh)). Not yet a real watch,\nand not the crown: `press` has no rotary input.\n\n**Windows is not supported yet.** Everything that needs no device, the\nnamed-pipe daemon transport included, passes in CI on a Windows runner, and\nsince 2026-10-22 an Android emulator and a phone have been driven from a\nWindows laptop, through the CLI, MCP and all five clients. What is left\nbefore it is supported is in [docs/WINDOWS.md](docs/WINDOWS.md).\n\nBoth device-side agents are pinned by version, verified against checksums\ncompiled into the binary, and refused on a mismatch.\n\n## App types\n\nWhat an app is built with decides where its elements are, so it decides what\nMobium reaches. Each type has been driven on a device:\n\n| Type | How Mobium reaches it | Driven here |\n| --- | --- | --- |\n| **Native** — Android SDK, Jetpack Compose, UIKit, SwiftUI | the platform's accessibility tree | Settings, Calculator, Clock, Wikipedia, F-Droid, Aegis, Seal (Compose); on iOS also NetNewsWire, Ice Cubes (SwiftUI), Pocket Casts and Kiwix |\n| **Hybrid** — a native shell around WebViews | the shell through the tree; each WebView as its own context, [below](#hybrid-apps-and-webviews) | Wikipedia's articles, MobiumApp's web screens |\n| **Mobile web and PWAs** — a page in a browser, or installed to the home screen | the browser's page as a context; on iOS a tap in the page is placed from text the page and the native tree both report | Safari, Chrome, Squoosh as a PWA — on Android a PWA's taps only while Chrome reports its WebView |\n| **Cross-platform, native once removed** — React Native | an ordinary native app: it renders real native views | MobiumApp, on both platforms and on real phones |\n| **Cross-platform that paints** — Flutter | the semantics tree Flutter publishes for accessibility, like any app | MobiumApp's Flutter demo, on both platforms |\n\nA browser is reachable but not managed — no tabs, no browser sessions; testing\na website is [Vibium](https://github.com/VibiumDev/vibium)'s job.\n[docs/APP-TYPES.md](docs/APP-TYPES.md) has what was measured for each type,\nwhat is checked in and what was driven only once.\n\n## Hybrid apps and WebViews\n\nA hybrid app's web content is not usable through the accessibility tree, so\nMobium attaches to the WebView, and switching context changes what `map` and\n`tap` operate on:\n\n```\n$ mobium contexts\nNATIVE_APP  (current)\nWEBVIEW_com.example  — Checkout https://shop.example/cart\n\n$ mobium context WEBVIEW_com.example\n$ mobium map\n@e1 Promo code (input)\n@e2 Apply (button)\n$ mobium tap @e2\ntapped @e2 at (462, 397) in WEBVIEW_com.example\n```\n\n**Reads go over the page's debugging protocol; taps stay native.** The page is\nmapped by a script, its CSS coordinates are converted to device pixels using\nthe WebView's on-screen frame and the page's visual viewport, and the tap is\ndelivered by the native driver like any other. Android WebViews speak CDP; iOS\nWKWebViews speak WebKit's Remote Web Inspector, over a Unix socket on a\nsimulator and through usbmuxd and lockdown on a phone.\n\nA WebView is reachable only if the app opted in —\n`WebView.setWebContentsDebuggingEnabled(true)` on Android, `isInspectable` on\niOS 16.4 and later. Neither can be forced from outside. Which kinds of app\nMobium drives, and what React Native and Flutter each put in the tree, is in\n[docs/APP-TYPES.md](docs/APP-TYPES.md).\n\n## Front doors\n\nThe CLI does not implement behavior. It calls the same tools an MCP client\ncalls, so the surfaces cannot drift:\n\n```mermaid\nflowchart LR\n    cli[\"CLI\"] --> daemon\n    clients[\"Python · JS · Go · Java · .NET\"] --> pipe[\"mobium pipe\"] --> daemon\n    mcp[\"MCP client\"] --> mcpcmd[\"mobium mcp\"]\n    daemon[\"daemon<br/>sessions, @refs\"] --> agent\n    mcpcmd --> agent[\"internal/agent<br/>every tool, written once\"]\n    agent --> driver[\"internal/mobiumdriver<br/>uiautomator2 · uiautomator ·<br/>wda · external\"]\n    agent --> webview[\"internal/webview<br/>CDP · Remote Web Inspector\"]\n    driver --> devices[\"emulators, phones,<br/>simulators, iPhones\"]\n    webview --> devices\n```\n\n`mobium inspect` serves the same tools as a page on this machine: the\nscreen with every element outlined, the locator for the one clicked, and\nwhat you do recorded as a test\n([docs/guides/inspector.md](docs/guides/inspector.md)).\n\n[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) has the full picture: every\nlayer, the package graph, and one call traced end to end.\n\nThe daemon starts on demand, holds the `@ref` table and exits after 30 idle\nminutes. `MOBIUM_SESSION=<name>` gives a run its own daemon, so two terminals\nor two CI jobs on one host stay isolated — and since one daemon serves one\ncall at a time, runs on different devices at once should each have one; every\nclient takes it as a `session` option ([SETUP.md](docs/SETUP.md#parallel-runs)).\n\n**MCP:**\n\n```sh\nclaude mcp add mobium -- mobium mcp\n```\n\n[The MCP guide](docs/guides/mcp.md) has other clients' configuration, an\nagent at work, and what the protocol carries; [the command-line\nguide](docs/guides/cli.md) is the same for `mobium` itself.\n\n**Agent skill:** [skills/mobile-check/SKILL.md](skills/mobile-check/SKILL.md) teaches the\nloop, the locators, WebView contexts, and that a command reporting success is\nnot evidence the app did anything.\n\n```sh\nnpx skills add mobiumdev/mobium --skill mobile-check\n```\n\n**Language clients** — Python, JavaScript, Go, Java and .NET, each covering the\nwhole tool surface with no dependencies:\n\n```python\nfrom mobium import start\n\nwith start(platform=\"android\", app=\"com.example.shop\") as device:   # quits when the block ends\n    sign_in = device.wait_for(\"text=Sign in\")\n    device.tap(sign_in.ref)\n```\n\n```go\nimport mobium \"github.com/mobiumdev/mobium/clients/go\"\n\ndev, err := mobium.Start(ctx, mobium.WithPlatform(\"ios\"), mobium.WithApp(\"com.example.shop\"))\ndefer dev.Quit(ctx)\nel, err := dev.WaitFor(ctx, \"text=Sign in\", nil)\nerr = dev.Tap(ctx, el.Ref)\n```\n\n`start` opens the session on the device and launches the app fresh; `quit`\nends it and puts back anything it changed for the session. On the command line they are `mobium session start` and\n`mobium session end`.\n\nEvery client spawns `mobium pipe`, which forwards to the shared daemon: a\ndevice-side server holds one session at a time, so a client with its own\nsession would invalidate the CLI's. The Go client is a separate module that\nneeds only the standard library; the Java and .NET clients write their own\nJSON so a test harness forces no library version onto the app under test. See\n[clients/java](clients/java/README.md) and [clients/dotnet](clients/dotnet/README.md).\n\n## Drivers for other platforms\n\nAnything Mobium does not drive itself — a TV, a desktop app, a platform nobody\nhas written yet — is a **driver**: an executable named `mobium-driver-<name>`\non your `PATH`, in any language, speaking JSON-RPC on stdio.\n\n```sh\nmobium map --driver roku\n```\n\nA driver needs three methods to be useful — `snapshot`, `screenshot` and\n`tap` — and inherits locators, `@ref`s, waiting, scrolling, the CLI, MCP and\nall five clients. [examples/drivers/](examples/drivers/) has the guide and a\ncomplete driver in dependency-free Python that produces the same map as the\nbuilt-in backend; [PROTOCOL.md](examples/drivers/PROTOCOL.md) is the\nprotocol.\n\n## Documentation\n\n| | |\n| --- | --- |\n| [docs/quickstart/](docs/quickstart/README.md) | using Mobium: install it and drive a device, from each client |\n| [docs/guides/](docs/guides/README.md) | the command line and MCP, auto-wait, gray box, `mobium test`, network conditions, and driving other machines' devices through a grid |\n| [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | working on Mobium: toolchains, `make ci`, device checks, pull requests |\n| [docs/PHILOSOPHY.md](docs/PHILOSOPHY.md) | *mutatis mutandis*: where the motto comes from, and the one design rule |\n| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | the layers, the package graph, and one call end to end |\n| [docs/SETUP.md](docs/SETUP.md) | attaching an emulator, a phone or a simulator |\n| [docs/SHUTDOWN.md](docs/SHUTDOWN.md) | stopping without orphaning a device session |\n| [docs/API.md](docs/API.md) | every tool and what reaches it (generated) |\n| [docs/FLAGS.md](docs/FLAGS.md) | every argument and the command that sets it (generated) |\n| [docs/APP-TYPES.md](docs/APP-TYPES.md) | native, web, hybrid and cross-platform apps |\n| [docs/GESTURES.md](docs/GESTURES.md) | every touch gesture and what each platform did |\n| [docs/BACK.md](docs/BACK.md) | back on Android and iOS — the key, the edge swipe, what apps do with them — and what is missing |\n| [docs/FORMFLUX.md](docs/FORMFLUX.md) | one device impersonating many screens |\n| [docs/CHALLENGES.md](docs/CHALLENGES.md) | platform behaviors found while building this, and how each is handled |\n| [docs/THREAT-MODEL.md](docs/THREAT-MODEL.md) | what can go wrong when an agent drives a phone, and what is done and open |\n| [docs/upstream/](docs/upstream/README.md) | problems in projects Mobium depends on, written up with a patch |\n| [docs/WINDOWS.md](docs/WINDOWS.md) | the state of Windows support |\n| [docs/ROADMAP.md](docs/ROADMAP.md) | what is next |\n| [docs/RELEASE-CHECKLIST.md](docs/RELEASE-CHECKLIST.md) | the device checks CI cannot run |\n| [examples/drivers/PROTOCOL.md](examples/drivers/PROTOCOL.md) | the protocol a third-party driver speaks |\n| [docs/checks/](docs/checks/) | end-to-end scripts that drive a real device |\n| [examples/drivers/](examples/drivers/) | writing a driver |\n| [CONTRIBUTING.md](CONTRIBUTING.md) | building, testing and the project's rules |\n\nSeveral checks drive **[MobiumApp](https://github.com/mobiumdev/mobium-app)**,\na React Native app built to be driven: each screen is a control for a case\nthat can go wrong, and on iOS it is the only way to reach an app's WebView,\nsince one that has not opted into inspection (`isInspectable`) is invisible\nto any debugger, and that cannot be forced from outside.\n\n## License\n\nApache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE). Code\npublished before this change was MIT licensed, and stays MIT for anyone who\nhas it.\n\nBuilt binaries statically link a few Go libraries under MIT, BSD and\nApache-2.0 licenses; they are listed, with their notices, in\n[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). The two device-side agents\n(the UiAutomator2 server, Apache-2.0, and WebDriverAgent, whose LICENSE file\nis BSD-3-Clause) are not bundled: Mobium downloads them from their upstream\nreleases at runtime, pinned and checksummed, and installs them on the device.\n"}