TL;DR — Mobile() is constructed at module import time with a hardcoded emulator-5554 default, so the process dies before the MCP handshake if that emulator isn't running. Because the Claude Desktop extension never passes --device, a physical handset can never be driven, and the failure surfaces to the user only as "Server disconnected".
Environment
|
|
| Android-MCP |
0.1.0 (installed as a Claude Desktop extension, ant.dir.gh.cursortouch.android-mcp) |
| Host |
Windows 11 (10.0.26200) |
| Python |
3.13 (uv-managed venv) |
| Launcher |
uv --directory <ext> run android-mcp (from manifest.json) |
| Device |
physical handset, adb devices reports state device |
| Emulator |
none running |
Steps to reproduce
- Attach and authorize a physical Android device. Confirm
adb devices lists it as device.
- Do not start an emulator.
- Start the MCP server the way the extension does:
uv --directory <ext> run android-mcp.
Actual result
The process raises during import and exits before serving anything:
uiautomator2.exceptions.ConnectError: device emulator-5554 not online
During handling of the above exception, another exception occurred:
File "src\android_mcp\__main__.py", line 26, in <module>
mobile=Mobile(device=args.device if args.device else 'emulator-5554')
File "src\android_mcp\mobile\service.py", line 14, in __init__
raise ConnectionError(f"Failed to connect to device {device}: {e}")
ConnectionError: Failed to connect to device emulator-5554: device emulator-5554 not online
The client sends initialize, gets a closed pipe, and reports:
[Android-MCP] [error] Server disconnected. For troubleshooting guidance, please visit our debugging documentation
Expected result
The server starts, advertises its tools, and drives the attached device — or, if no device is reachable, still starts and returns an actionable error from the first tool call.
Root cause
Two independent problems compound:
1. Connection happens at import, so failure is fatal. __main__.py line 26 runs at module scope:
mobile=Mobile(device=args.device if args.device else 'emulator-5554')
device=mobile.get_device()
A device is a runtime-variable resource — unplugged, asleep, unauthorized, revoked mid-session — but binding it at import turns any transient absence into a dead server. The MCP client can't distinguish "server is broken" from "phone is unplugged", so the user sees only a generic disconnect.
2. There is no way to specify a device through the extension. --device exists in the ArgumentParser, but manifest.json hardcodes the launch args with no user_config block:
"mcp_config": {
"command": "uv",
"args": ["--directory", "${__dirname}", "run", "android-mcp"]
}
So the default always wins, and an attached handset is never even enumerated. The README's "Ensure ADB is installed and your device is connected/authorized" implies physical devices are supported, but through the extension they cannot be.
Suggested fix
Three changes, roughly in order of value:
-
Connect lazily. Construct Mobile on first tool use (or in the existing lifespan hook) rather than at import, and surface connection failures as tool errors. The lifespan context manager is already defined and unused — it's a natural home, though a per-call lazy accessor also allows recovery after a reconnect without restarting the server.
-
Auto-detect when --device is absent. uiautomator2 already depends on adbutils, so picking the first online device costs almost nothing:
def resolve_device(preferred=None):
if preferred:
return preferred
serials = [d.serial for d in adbutils.adb.list() if d.state == 'device']
if not serials:
raise ConnectionError('No online ADB devices. Attach and authorize a device, or pass --device.')
return serials[0]
Erroring with an explicit message when nothing is attached beats silently falling back to emulator-5554, which is what makes the current failure so opaque.
- Expose the serial in
manifest.json. Add a user_config entry for the device serial and thread it into the args, so users with several devices attached can choose without editing files inside the extension directory.
I patched (2) locally to confirm the diagnosis — the server came up immediately and bound the physical device, no other changes needed:
Android-MCP: auto-selected device 652B...0012
FastMCP 2.14.0 | Server name: Android-MCP | Transport: STDIO
INFO Starting MCP server 'Android-MCP' with transport 'stdio'
I've since reverted it to stay on the stock install. Happy to open a PR for any of the three if that's useful.
Workaround for anyone else hitting this
Start an emulator before launching Claude Desktop — the first AVD gets serial emulator-5554 — or edit src/android_mcp/__main__.py in the installed extension directory as above, accepting that an extension update will overwrite it.
TL;DR —
Mobile()is constructed at module import time with a hardcodedemulator-5554default, so the process dies before the MCP handshake if that emulator isn't running. Because the Claude Desktop extension never passes--device, a physical handset can never be driven, and the failure surfaces to the user only as "Server disconnected".Environment
ant.dir.gh.cursortouch.android-mcp)uv --directory <ext> run android-mcp(frommanifest.json)adb devicesreports statedeviceSteps to reproduce
adb deviceslists it asdevice.uv --directory <ext> run android-mcp.Actual result
The process raises during import and exits before serving anything:
The client sends
initialize, gets a closed pipe, and reports:Expected result
The server starts, advertises its tools, and drives the attached device — or, if no device is reachable, still starts and returns an actionable error from the first tool call.
Root cause
Two independent problems compound:
1. Connection happens at import, so failure is fatal.
__main__.pyline 26 runs at module scope:A device is a runtime-variable resource — unplugged, asleep, unauthorized, revoked mid-session — but binding it at import turns any transient absence into a dead server. The MCP client can't distinguish "server is broken" from "phone is unplugged", so the user sees only a generic disconnect.
2. There is no way to specify a device through the extension.
--deviceexists in theArgumentParser, butmanifest.jsonhardcodes the launch args with nouser_configblock:So the default always wins, and an attached handset is never even enumerated. The README's "Ensure ADB is installed and your device is connected/authorized" implies physical devices are supported, but through the extension they cannot be.
Suggested fix
Three changes, roughly in order of value:
Connect lazily. Construct
Mobileon first tool use (or in the existinglifespanhook) rather than at import, and surface connection failures as tool errors. Thelifespancontext manager is already defined and unused — it's a natural home, though a per-call lazy accessor also allows recovery after a reconnect without restarting the server.Auto-detect when
--deviceis absent.uiautomator2already depends onadbutils, so picking the first online device costs almost nothing:Erroring with an explicit message when nothing is attached beats silently falling back to
emulator-5554, which is what makes the current failure so opaque.manifest.json. Add auser_configentry for the device serial and thread it into the args, so users with several devices attached can choose without editing files inside the extension directory.I patched (2) locally to confirm the diagnosis — the server came up immediately and bound the physical device, no other changes needed:
I've since reverted it to stay on the stock install. Happy to open a PR for any of the three if that's useful.
Workaround for anyone else hitting this
Start an emulator before launching Claude Desktop — the first AVD gets serial
emulator-5554— or editsrc/android_mcp/__main__.pyin the installed extension directory as above, accepting that an extension update will overwrite it.