Provides two async clients ( PortalHTTPClient and PortalWSClient), a configuration helper ( PortalConfig), and a built-in MCP server for communicating with Android devices running DroidRun Portal. All client methods are async and support async with context managers.
bash
cd droidrun-agent && uv sync # core only
cd droidrun-agent && uv sync --extra mcp # with MCP server support
Communicates with Portal's HTTP server (default port 8080) using Bearer token authentication.
python
from droidrun_agent import PortalHTTPClient
async with PortalHTTPClient(base_url="http://192.168.1.100:8080", token="YOUR_TOKEN") as client:
await client.ping()
state = await client.get_state_full()
await client.tap(200, 400)
png = await client.take_screenshot()
SignatureReturn TypeDescriptionping()``dictHealth check, no auth requiredget_a11y_tree()``dictSimplified accessibility treeget_a11y_tree_full(*, filter: bool = True)``dictFull accessibility tree, filter=False keeps small elementsget_state()``dictSimplified UI stateget_state_full(*, filter: bool = True)``dictFull UI state (a11y_tree + phone_state), filter=False keeps small elementsget_phone_state()``dictPhone state info (current app, activity, keyboard status, etc.)get_version()``strPortal app version stringget_packages()``list[dict]List of launchable apps, each containing packageName, label, etc.take_screenshot(*, hide_overlay: bool = True)``bytesDevice screenshot as PNG bytes, hide_overlay=False to show overlay
SignatureReturn TypeDescriptiontap(x: int, y: int)``dictTap screen coordinatesswipe(start_x: int, start_y: int, end_x: int, end_y: int, duration: int | None = None)``dictSwipe gesture, duration is optional duration in millisecondsglobal_action(action: int)``dictExecute Android accessibility global action (1=Back, 2=Home, 3=Recents)start_app(package: str, activity: str | None = None, stop_before_launch: bool = False)``dictLaunch an appstop_app(package: str)``dictBest-effort stop an appinput_text(text: str, clear: bool = True)``dictInput text (auto base64-encoded), clear=True clears field firstclear_input()``dictClear the focused input fieldpress_key(key_code: int)``dictSend Android key code (e.g. 66=Enter, 3=Home, 4=Back)set_overlay_offset(offset: int)``dictSet overlay vertical offset in pixelsset_socket_port(port: int)``dictUpdate the HTTP server port
Communicates with Portal's WebSocket server (default port 8081) using JSON-RPC style messages. Automatically reconnects when a method is called on a broken connection.
python
from droidrun_agent import PortalWSClient
async with PortalWSClient("ws://192.168.1.100:8081", token="YOUR_TOKEN") as ws:
await ws.tap(200, 400)
state = await ws.get_state()
png = await ws.take_screenshot()
time_ms = await ws.get_time()
Supports all action methods from PortalHTTPClient ( tap, swipe, global_action, start_app, stop_app, input_text, clear_input, press_key, set_overlay_offset, set_socket_port, take_screenshot) with identical signatures.
Query methods:
SignatureReturn TypeDescriptionget_packages()``AnyList of launchable packagesget_state(*, filter: bool = True)``AnyFull state, filter=False keeps small elementsget_version()``AnyPortal version stringget_time()``AnyDevice Unix timestamp in millisecondsinstall(urls: list[str], hide_overlay: bool = True)``AnyInstall APK(s) from URL(s), supports split APKs (WebSocket only)
WebSocket screenshots automatically parse binary frames and return PNG bytes directly.
All exceptions inherit from PortalError:
ExceptionTriggerPortalErrorBase exceptionPortalConnectionErrorCannot connect to Portal serverPortalAuthErrorInvalid or missing token (HTTP 401/403)PortalTimeoutErrorRequest timed outPortalResponseErrorServer returned unexpected status or error
python
import asyncio
from droidrun_agent import PortalHTTPClient, PortalWSClient
async def demo_http():
async with PortalHTTPClient("http://localhost:8080", token="YOUR_TOKEN") as client:
print(await client.ping())
print("Version:", await client.get_version())
print("Packages:", len(await client.get_packages()))
await client.tap(500, 800)
await client.swipe(500, 1500, 500, 500, duration=300)
await client.input_text("Hello World")
await client.press_key(66) # Enter
state = await client.get_state_full()
png = await client.take_screenshot()
print(f"Screenshot: {len(png)} bytes")
async def demo_ws():
async with PortalWSClient("ws://localhost:8081", token="YOUR_TOKEN") as ws:
print("Version:", await ws.get_version())
print("Time:", await ws.get_time())
await ws.tap(500, 800)
await ws.start_app("com.android.settings")
png = await ws.take_screenshot()
print(f"Screenshot: {len(png)} bytes")
asyncio.run(demo_http())
asyncio.run(demo_ws())
Helper dataclass for managing connection settings. Supports direct construction or loading from environment variables.
python
from droidrun_agent import PortalConfig
# Direct construction
config = PortalConfig(base_url="http://192.168.1.100:8080", token="YOUR_TOKEN")
client = config.create_client()
# Load from environment variables
config = PortalConfig.from_env()
client = config.create_client()
FieldTypeDefaultDescriptionbase_url``str(required)Portal HTTP or WebSocket base URLtoken``str(required)Bearer authentication tokentimeout``float``10.0Request timeout in secondstransport``str``"http"``"http" or "ws"
Environment variables for from_env(): PORTAL_BASE_URL, PORTAL_TOKEN, PORTAL_TIMEOUT, PORTAL_TRANSPORT.
A built-in MCP (Model Context Protocol) server exposes all Portal operations as tools for AI agent integration. Requires the mcp optional dependency ( pip install droidrun-agent[mcp]).
bash
# Via CLI entry point
droidrun-agent --mcp
# Or as a Python module
python -m droidrun_agent --mcp
The server reads PORTAL_BASE_URL, PORTAL_TOKEN, PORTAL_TIMEOUT, and PORTAL_TRANSPORT from environment variables and communicates over stdio.
ToolDescriptionportal_pingHealth check (HTTP only)portal_tapTap screen coordinatesportal_swipeSwipe gestureportal_screenshotTake screenshot, returns PNG imageportal_get_stateGet simplified UI stateportal_get_state_fullGet full UI state (a11y tree + phone state)portal_get_a11y_treeGet simplified accessibility tree (HTTP only)portal_get_a11y_tree_fullGet full accessibility tree (HTTP only)portal_get_phone_stateGet phone state info (HTTP only)portal_get_versionGet Portal app versionportal_get_packagesList launchable packagesportal_global_actionExecute accessibility global action (1=Back, 2=Home, 3=Recents)portal_start_appLaunch an app by package nameportal_stop_appStop an appportal_input_textInput text into focused fieldportal_clear_inputClear focused input fieldportal_press_keySend Android key code (66=Enter, 3=Home, 4=Back)portal_set_overlay_offsetSet overlay vertical offsetportal_get_timeGet device timestamp (WebSocket only)portal_installInstall APK(s) from URL(s) (WebSocket only)
Register as an openclaw MCP skill:
json
{
"mcpServers": {
"droidrun-agent": {
"command": "uvx",
"args": ["--with", "mcp", "droidrun-agent", "--mcp"],
"env": {
"PORTAL_BASE_URL": "http://192.168.1.100:8080",
"PORTAL_TOKEN": "YOUR_TOKEN"
}
}
}
}