The Android
client for the SealGate stdio tunnel — a device-side daemon that lets cloud
agents reach MCP (Model Context Protocol) servers running on your phone. It holds
a single outbound WebSocket to the SealGate backend, so there is no inbound
port to open, and your data and logins stay on the device.
Based on the design in Stdio Tunnels: Bridging Cloud Agents to Local MCPs.
The tunnel speaks the same wire protocol as the desktop sealgate-stdiod
daemon (protocol v2; see schema/tunnel-protocol.json). Where the desktop
daemon spawns npx/uvx subprocesses, this app answers MCP requests from
in-process Kotlin modules — a phone can't spawn stdio servers.
The app exposes one built-in MCP server, conventionally registered under the
SealGate prefix mobilebash. The prefix is only a label: the dashboard
command mobile-builtin is what binds a server to this module, so any prefix
works (prefixes are unique per organisation, so a second phone in the same org
needs a different one).
Its single run tool executes the required script argument
inside a restricted just-bash
environment:
battery status | jq '.level_percent'
bluetooth scan --timeout-ms 5000 > /tmp/scan.json
jq '.devices[] | select(.rssi > -70)' /tmp/scan.jsonRun device --help, battery --help, wifi --help, bluetooth --help,
usb --help, camera --help, or (in non-Play builds) computer --help to discover the Android CLI. It delegates to the same Kotlin
capability modules, including Bluetooth and USB control operations, so Android runtime
permissions and on-device permission dialogs still apply.
| Command | What it does |
|---|---|
device |
Device model, OS build, and identifiers. |
battery |
Charge level and charging state. |
wifi |
Wi-Fi connection status. |
bluetooth |
BLE + classic Bluetooth: status, scan, GATT read/write, notify/indicate, RFCOMM/SPP, and pairing. |
usb |
USB-OTG host access: enumerate devices, request permission, and raw bulk/control transfers. |
camera |
Still-photo capture (status, list, snap --lens front|back --flash off|on|auto --zoom --width --height --quality), returning the photo as a native MCP image. Off by default; enable the in-app toggle and grant the Camera permission. |
computer |
Optional private-build cross-app observation and control, returning an MCP image and accessibility tree together. |
Computer control is compiled into debug, private, and enterprise builds only;
the Play/release manifest contains no accessibility service. It defaults off
and requires both the in-app toggle and explicit activation in Android's
Accessibility settings. computer observe and post-action results carry the
screenshot as native MCP image content and the matching accessibility tree as
structured content. The 1 MiB Bash output limit remains separate from a bounded
4 MiB typed MCP result.
The virtual filesystem is shared across calls for one tunnel run and destroyed when that run stops. Shell-local variables, functions, aliases, and working directory reset after each call. Mobile Bash has no Android filesystem, real process, general network, Python, JavaScript, or SQLite access. It enforces a 60-second call deadline, a 64 KiB script limit, 1 MiB output limit, 8 MiB per-file limit, and 32 MiB total virtual filesystem limit.
- The daemon opens one outbound WebSocket — no incoming ports, works behind NAT and mobile carriers.
- After a
client_hello/server_hellohandshake it exchanges symmetricmcp_frames (opaque JSON-RPC bodies), routed to built-in modules by server name. - It runs as an Android foreground service so the OS keeps it alive, and reconnects forever with jittered exponential backoff.
- Android Studio (Ladybug / 2024.2+ recommended)
- JDK 17+
- Android SDK Platform 35, min SDK 26
-
Open the project in Android Studio (File → Open, select this folder) and let it sync, or build from the command line:
./gradlew assembleDebug # build the debug APK ./gradlew testDebugUnitTest # run JVM unit tests ./gradlew installDebug # install on a connected device/emulator
-
Run the app, confirm (or edit) the gateway WebSocket URL, and tap Sign in with SealGate. The app runs the OAuth 2.0 device-authorization flow (RFC 8628, with PKCE): it shows a short code and opens the dashboard's device page, where you approve the phone with one click. On approval the app receives a scoped
ewc_tunnel credential (never a human API key) bound to a backend-issued device id, stores it, and starts the tunnel. Settings persist across restarts; the ongoing notification shows the live connection state.Pasting a SealGate API key under Or connect with an API key and tapping Connect still works as an alternative to signing in.
Sign-in reuses the shared device-auth flow the desktop daemon uses, under a dedicated
mobileclient id; the backend side lives inedison-watch(src/api/v1/routes/device_auth.py,dev-docs/architecture/mobile-hardware-gateway-design.md).The credential (and the in-flight PKCE verifier of an interrupted sign-in) is stored encrypted at rest with an AES-256-GCM key held in the AndroidKeyStore (
SecretCipher), so a prefs dump or a backup restored to another phone cannot lift it. To disconnect, open settings and tap Sign out: the app stops the tunnel, forgets the local credential, and revokes the installation in the dashboard (POST /api/v1/auth/device/revoke). If the gateway later revokes the credential itself, the tunnel stops reconnecting and the app asks you to sign in again instead of looping.While the tunnel is running, pull down from the top of the app screen to close the current socket and reconnect immediately with the saved settings.
In the SealGate dashboard, add one local server for the device with display name Mobile Bash, MCP prefix
mobilebash, and commandmobile-builtin. No arguments are required. The resulting agent tool ismobilebash_run. Ifmobilebashis already taken in your organisation, pick any other prefix (saymobilebash-alice); the command is what matters, and the tool becomes<prefix>_run.
While the service runs, it posts an ongoing notification:
Android capabilities are in-process Kotlin modules behind the Mobile Bash CLI. To add one:
- Extend
BaseMcpModule(seemcp/DeviceInfoModule.kt) — supply the tool descriptors and thetools/callhandler; the MCP lifecycle (initialize,ping,tools/list) is handled for you. - Register it in the capability list in
TunnelService.connect. - Map its commands in
MobileCommandRouterand add CLI-focused tests. Do not register another dashboard server or expose the module as a separate MCP surface.
See LICENSE.
The embedded just-bash runtime and QuickJS bridge are Apache-2.0 dependencies; see THIRD_PARTY_NOTICES.md.
