ADB MCP Server — let an AI assistant actually operate your Android devices

My Fire TV came back from a factory reset with nothing on it. Instead of an evening with a keyboard, I described what I wanted in a chat window and watched eight apps get installed, two keyboards enabled and the right one set as system IME — including a split-APK app that adb install refuses outright. That was the moment this stopped being a toy.

Repo: GitHub - st412m/ha-adb-mcp · GitHub

It’s a Home Assistant addon that exposes network ADB over MCP (Model Context Protocol), so an assistant connected to your HA instance can run shell commands, take screenshots, dump the UI tree and tap coordinates, type text (including non-ASCII), install and remove apps, move files, and read logcat — on any Android device on your LAN.

Why not the official MCP Server integration?

Fair question, and they don’t compete. The official integration exposes the HA conversation agent — the intents your assistant already understands: turn on a light, set a temperature. It’s the right tool when what you want is an entity.

This addon sits a layer below that. It talks to Android directly, so the assistant can do things HA has no entity for: read a crash log, dump the current UI and tap a coordinate, sideload an APK, disable a preinstalled package, pull a file off the box. If your device is an Android TV that HA already tracks via the androidtv integration, the two coexist (details below) — this just operates where entities don’t exist.

What I actually use it for

  • Debloating a Fire TV. Listing what’s installed, deciding what’s safe to disable, and doing it — with the reasoning visible in the chat rather than copy-pasted from a forum thread.
  • Restoring a box after a reset. pm path <pkg> on a working device lists the exact split set of an app; pull those, pass them back as an array, done.
  • Diagnosing. “Why did this app die last night” is a logcat question, and filtering runs on-device so a huge buffer never crosses the wire.
  • UI automation. adb_ui_dump returns a compact element list with tap coordinates, so “open the app and go to the third settings tab” works without screenshot guesswork.

Coexistence with the androidtv integration

Android’s adbd doesn’t tolerate two independent TCP clients, and the integration connects directly by default — the sessions fight. The addon runs a classic adb server on 5037; point the integration at ADB server = HA host IP, port 5037 and both share one daemon and one device session. Note that adb_server_ip isn’t in the integration’s options flow, so switching an existing entry means deleting and re-adding it (entity IDs survive if the MAC is unchanged).

Honest limitations

  • ADB is not a sandbox. adb_uninstall takes app data with it, and pm uninstall --user 0 on a system package can leave a device broken or unbootable. An assistant will execute what it’s asked to. There’s an allow_shell: false mode that disables the raw shell and keeps screenshots and UI control working — a sensible place to start.
  • Auth is a secret token in the URL path. One layer. Use a long random value and HTTPS. Never expose port 5037 outside your LAN — the adb server has no auth at all.
  • The tool list is cached per chat by the client, so after an addon update an open chat keeps the old schemas.
  • ~60 s gateway timeout per call. Background long-running commands on-device and poll instead of blocking.
  • Screenshots wake devices, and HDMI-CEC will happily switch on the TV attached. Worth knowing before scripting a screenshot loop.
  • aarch64 is build-verified, not device-verified — clean build and start on a Pi 4, but no Android device was attached on that setup.

On the engineering, since it’s relevant to trust

A soak test caught a memory leak in the screenshot path early on: about 3.1 MB retained per frame, ratcheting, never released. It was reproduced in two independent runs, traced to the streaming pipeline, fixed by going file→file, and re-verified before 1.0.0 — 24 frames now add under 2 MB total, with per-series deltas decaying to noise. I mention it not because the bug was interesting but because “an addon that hands an AI a shell on your devices” deserves more than
“works on my machine”, and I’d rather show the numbers than assert it.

Feedback, issues and reports from other architectures all welcome.

Suggestion:
Add-ons have been renamed to Apps about a year ago now.

Add-ons no longer exist and building new Apps and using the words and links to the old way in the readme is very confusing for users wanting to try your stuff…

Please consider a wording revision.
A simpler approach may be to use the Create a link – My Home Assistant facility…

For instance, this install sequence from your readme doesn’t work:

Settings → Add-ons → Add-on Store → ⋮ → Repositories → add

Proofreading and testing the actual instructions that you copied from other sources to make sure things are right is a good best practice to follow. So is this: AI Policy | Home Assistant Developer Docs

The ADB-over-MCP layer is genuinely interesting, but the architecture question I’d push on is where this produces new capability rather than re-wrapping what adb-over-tcp or ha-shell-command already gives you. A pattern some teams use when building bridge integrations is to identify one operation that’s impossible without the new layer, not just more convenient, and let that be the wedge. For ha-adb-mcp, that might be stateful multi-step device flows driven by an LLM context window — iterating through screen states without re-issuing adb shell each time, which neither raw ADB nor HA automations can coordinate natively.

On the test-report side, I’d be most curious about failure mode behavior when the ADB device disconnects mid-session and whether MCP callers get a clean error signal or a hang. That’s often where bridge layers expose their real fault boundary. If you have a repeatable test harness for that, sharing the fixture would probably get you more useful architecture feedback than general “does it work” reports.

Thanks - that’s a fair catch. The README was pointing people at a path that no longer exists.-

Fixed: terminology is now Apps throughout, and the install step reads Settings → Apps → App Store → ⋮ → Repositories → + Add. I also added the my.home-assistant.io badge you suggested.

One thing worth knowing, since I tested it before shipping it: the supervisor_add_addon_repository redirect currently doesn’t do what it says. The intermediate page renders correctly, but after “Open link” the App Store opens with no dialog and repository_url is silently dropped - my.home-assistant.io#698, open since April with no maintainer response. So the badge is there, but I kept the manual path next to it and linked the issue. Otherwise people click, nothing happens, and they conclude the repository is broken.

I left the repository name and config.yaml alone: the 2026.2 rename covered the UI and the docs, while the technical artifacts still use add-on. I’ve said that explicitly in the README so it doesn’t look like something I missed.

The disconnect question is a good one, and I can answer it concretely, because it’s in the code rather than on a roadmap.

Every ADB invocation goes through execFile under a hard timeout, and the process is killed when it expires. Currently: connect/disconnect 10 s, pair and logcat 20 s, screenshot / ui_dump / tap / swipe / key / text 30 s, shell 30 s with a 120 s ceiling via a timeout_sec parameter, uninstall 60 s, push/pull 120 s, install 180 s. A device that sleeps, reboots or falls off the network fails the call instead of hanging it: what comes back is an ordinary MCP tool result with isError: true and a text message. There is no stalled-request path.

On top of that, the common failures are rewritten into the action that fixes them. device offline returns “the TCP session died (device slept or rebooted) - run adb_disconnect for this host, then adb_connect again”, and device not found points at adb_devices. That matters for an assistant specifically: it can recover on its own instead of asking a human what happened.

There’s no dedicated harness for this, and I don’t think one is needed - it reproduces by hand in about a minute: put the box to sleep, or pull it off the network, in the middle of adb_shell 'sleep 120'. There is one honest boundary, which I’ve now written into the README: install, push, pull and uninstall are allowed more time than the ~60 s gateway timeout, and the auth proxy sets no timeout of its own - it’s a plain pipe. So on a genuinely long operation the thing that gives up first is your reverse proxy, not the app. The full table is under “Timeouts and failure behaviour”.

On where the new capability lives rather than the convenience: the post has two examples of it. The ui_dump → tap → dump-again loop, where the next action depends on what’s currently on screen, and split-APK restore, where the set of splits is decided by querying the device. Neither reduces to a fixed sequence of adb commands, because the branching only exists at runtime.

A small update: 1.2.4 is out since the original post.

Three things have appeared, each closing a specific pain point.

Package management with guard rails. This is the thing I wanted most and was most reluctant to build. Letting an assistant disable or remove packages is a straight road to a bricked box: one unlucky package and the device loses its launcher or its account registration - and you don’t find out at the time, you find out a day later when the store won’t open. So the tool is built on the assumption that it will eventually be asked to do something stupid. Anything that changes state only shows you the plan by default and does nothing. The list of untouchable packages isn’t written into the code; it’s asked of the device itself - what is the launcher right now, what is the keyboard, what handles accounts. If one of those turns up in a request, the whole operation is cancelled rather than half-applied. Between batches it checks whether any accounts have gone missing. And everything it did is recorded, so undoing it is a single call.

Installing apps that ship as bundles. A lot of apps now come as a single .apks file, which adb simply won’t install. That used to mean doing it by hand: unpack it, work out which parts this particular device needs, feed them in as a list. On Fire TV you couldn’t even do that on the device itself. Now you just point at the file and the right parts are picked from the actual properties of the box or phone. There’s a preview, so you can see what would be installed before anything is.

Finding something on screen and pressing it in one step. This used to be three: dump the screen layout, find the coordinates, tap them. Now you can just say what to press. And this turned up something interesting: on TV boxes, tapping coordinates doesn’t work the way you’d expect at all - it activates whatever currently has focus, so it silently hits the wrong thing. So the tool works out what kind of device it’s talking to, and on a TV box it walks the focus across with the arrow keys, checking after every step that the focus actually moved. If it can’t get there, it tells you where it got stuck and presses nothing - which I think matters more than making a guess.

What got better. Nearly every fix in the recent versions came from real devices rather than from thinking about it. A phone on Android 16 and a watch on Wear OS joined the three TV boxes, and each new kind of device turned up something the others couldn’t show. The boxes were too similar to each other, and a whole half of one feature had never actually run until a phone with a real touchscreen showed up. The principle all of this settled into: assume nothing about the device, ask it instead - and don’t report success for anything you haven’t checked. If an app was launched, it’s because the app was confirmed to be on screen, not because a command came back without an error.

Per-version detail is in the CHANGELOG, and the README has been filled in too.