mcp-adb/src/models.py
Ryan Malloy 3614ba8f8f Replace dict returns with typed Pydantic response models across all 65 tools
Every tool now returns a structured BaseModel instead of dict[str, Any],
giving callers attribute access, IDE autocomplete, and schema validation.
Adds ~30 model classes to models.py and updates all test assertions.
2026-02-11 03:57:25 -07:00

429 lines
16 KiB
Python

"""Pydantic models for Android ADB MCP Server."""
from typing import Any
from pydantic import BaseModel, Field
# ── Data Models (not tool results) ──────────────────────────────────
class DeviceInfo(BaseModel):
"""Android device information returned by ADB."""
device_id: str = Field(description="Unique device identifier/serial number")
status: str = Field(
description="Device connection status",
json_schema_extra={
"examples": ["device", "offline", "unauthorized", "no permissions"]
},
)
model: str | None = Field(None, description="Device model name")
product: str | None = Field(None, description="Product name")
class CommandResult(BaseModel):
"""Result of an ADB command execution (internal)."""
success: bool = Field(description="Whether the command succeeded")
stdout: str = Field(default="", description="Standard output from command")
stderr: str = Field(default="", description="Standard error from command")
returncode: int = Field(description="Command exit code")
# ── Base Result ─────────────────────────────────────────────────────
class ADBResult(BaseModel):
"""Base result for all ADB tool operations."""
success: bool = Field(description="Whether the operation succeeded")
error: str | None = Field(None, description="Error message if operation failed")
class ActionResult(ADBResult):
"""Result of a simple action (tap, press, toggle, etc.)."""
action: str = Field(description="Action that was performed")
# ── Screenshot / Screen ─────────────────────────────────────────────
class ScreenshotResult(ADBResult):
"""Screenshot capture operation result."""
local_path: str | None = Field(
None, description="Absolute path to the saved screenshot file"
)
class ScreenSizeResult(ADBResult):
"""Screen resolution information."""
width: int | None = Field(None, description="Screen width in pixels")
height: int | None = Field(None, description="Screen height in pixels")
raw: str | None = Field(None, description="Raw wm size output")
class ScreenDensityResult(ADBResult):
"""Screen density information."""
dpi: int | None = Field(None, description="Screen density in DPI")
raw: str | None = Field(None, description="Raw wm density output")
class RecordingResult(ADBResult):
"""Screen recording result."""
local_path: str | None = Field(None, description="Path to saved recording file")
duration_seconds: int | None = Field(None, description="Actual recording duration")
class ScreenSetResult(ActionResult):
"""Screen size override result."""
width: int | None = Field(None, description="New width in pixels")
height: int | None = Field(None, description="New height in pixels")
# ── Input ────────────────────────────────────────────────────────────
class InputResult(ActionResult):
"""Result of an input simulation action."""
coordinates: dict[str, int] | None = Field(
None, description="Tap/press coordinates {x, y}"
)
key_code: str | None = Field(None, description="Key code sent")
text: str | None = Field(None, description="Text typed or on clipboard")
duration_ms: int | None = Field(None, description="Duration in ms")
class SwipeResult(ActionResult):
"""Result of a swipe gesture."""
start: dict[str, int] = Field(description="Start coordinates {x, y}")
end: dict[str, int] = Field(description="End coordinates {x, y}")
duration_ms: int = Field(description="Swipe duration in ms")
class ClipboardSetResult(ActionResult):
"""Result of a clipboard set operation."""
text: str = Field(description="Text placed on clipboard (preview)")
pasted: bool | None = Field(None, description="Whether paste was performed")
paste_error: str | None = Field(None, description="Paste error if any")
class ShellResult(ADBResult):
"""Result of a shell command execution."""
command: str = Field(description="Command that was executed")
stdout: str = Field(default="", description="Standard output")
stderr: str = Field(default="", description="Standard error")
returncode: int = Field(default=0, description="Exit code")
# ── Apps ─────────────────────────────────────────────────────────────
class AppActionResult(ActionResult):
"""Result of an app management action."""
package: str | None = Field(None, description="Target package name")
url: str | None = Field(None, description="URL opened")
output: str | None = Field(None, description="Command output")
apk: str | None = Field(None, description="APK path")
kept_data: bool | None = Field(None, description="Whether data was preserved")
cancelled: bool | None = Field(None, description="Whether operation was cancelled")
message: str | None = Field(None, description="Status message")
class AppCurrentResult(ADBResult):
"""Currently focused app information."""
package: str | None = Field(None, description="Foreground package name")
activity: str | None = Field(None, description="Current activity class")
raw: str | None = Field(
None, description="Raw dumpsys output (if no package found)"
)
class PackageListResult(ADBResult):
"""List of installed packages."""
packages: list[str] = Field(default_factory=list, description="Package names")
count: int = Field(default=0, description="Number of packages")
class IntentResult(ActionResult):
"""Result of an intent-based operation."""
component: str | None = Field(None, description="Activity component")
intent_action: str | None = Field(None, description="Intent action")
data_uri: str | None = Field(None, description="Data URI")
broadcast_action: str | None = Field(None, description="Broadcast action sent")
package: str | None = Field(None, description="Target package")
output: str | None = Field(None, description="Command output")
# ── Devices ──────────────────────────────────────────────────────────
class DeviceSelectResult(ADBResult):
"""Device selection/status result."""
device: dict[str, Any] | str | None = Field(
None, description="Selected device info"
)
message: str | None = Field(None, description="Status message")
available: list[str] | dict[str, Any] | None = Field(
None, description="Available devices"
)
cached_info: Any | None = Field(None, description="Cached device info (if any)")
class DeviceInfoResult(ADBResult):
"""Comprehensive device information."""
battery: dict[str, Any] | None = Field(
None, description="Battery state (level, status, plugged)"
)
ip_address: str | None = Field(None, description="Device IP address")
wifi_ssid: str | None = Field(None, description="Connected WiFi SSID")
model: str | None = Field(None, description="Device model")
manufacturer: str | None = Field(None, description="Device manufacturer")
device_name: str | None = Field(None, description="Device codename")
android_version: str | None = Field(None, description="Android version")
sdk_version: str | None = Field(None, description="SDK API level")
storage: dict[str, int] | None = Field(
None, description="Storage info (total_kb, used_kb, available_kb)"
)
class RebootResult(ActionResult):
"""Device reboot result."""
mode: str = Field(description="Reboot mode (normal, recovery, bootloader)")
cancelled: bool | None = Field(None, description="Whether cancelled")
message: str | None = Field(None, description="Status message")
class LogcatResult(ADBResult):
"""Logcat capture result."""
action: str | None = Field(None, description="Action (logcat_clear)")
lines_requested: int | None = Field(None, description="Lines requested")
filter: str | None = Field(None, description="Filter spec applied")
output: str | None = Field(None, description="Log output")
# ── Connectivity ─────────────────────────────────────────────────────
class ConnectResult(ADBResult):
"""ADB network connection result."""
address: str = Field(description="Device address (host:port)")
output: str | None = Field(None, description="ADB command output")
already_connected: bool | None = Field(
None, description="Whether already connected"
)
class TcpipResult(ADBResult):
"""TCP/IP mode switch result."""
port: int | None = Field(None, description="ADB TCP port")
device_ip: str | None = Field(None, description="Device IP address")
connect_address: str | None = Field(
None, description="Address for adb_connect (ip:port)"
)
message: str | None = Field(None, description="Status message")
class DevicePropertiesResult(ADBResult):
"""Batch device properties result."""
identity: dict[str, str] | None = Field(
None, description="Identity props (model, manufacturer, serial)"
)
software: dict[str, str] | None = Field(
None, description="Software props (android version, sdk, build)"
)
hardware: dict[str, str] | None = Field(
None, description="Hardware props (chipset, ABI)"
)
system: dict[str, str] | None = Field(
None, description="System props (timezone, locale)"
)
# ── UI ───────────────────────────────────────────────────────────────
class UIDumpResult(ADBResult):
"""UI hierarchy dump result."""
xml: str | None = Field(None, description="Raw XML hierarchy")
clickable_elements: list[dict[str, Any]] = Field(
default_factory=list, description="Interactive elements"
)
element_count: int = Field(default=0, description="Number of interactive elements")
class UIFindResult(ADBResult):
"""UI element search result."""
matches: list[dict[str, Any]] = Field(
default_factory=list, description="Matching elements"
)
count: int = Field(default=0, description="Number of matches")
class WaitResult(ADBResult):
"""Wait/poll operation result."""
found: bool | None = Field(None, description="Whether text was found")
gone: bool | None = Field(None, description="Whether text disappeared")
element: dict[str, Any] | None = Field(None, description="Found element info")
wait_time: float | None = Field(None, description="Time waited in seconds")
attempts: int = Field(default=0, description="Poll attempts made")
class TapTextResult(ActionResult):
"""Tap-by-text result."""
text: str | None = Field(None, description="Text searched for")
coordinates: dict[str, int] | None = Field(None, description="Tapped coordinates")
element: dict[str, Any] | None = Field(None, description="Element that was tapped")
# ── Files ────────────────────────────────────────────────────────────
class FileTransferResult(ActionResult):
"""File push/pull result."""
local_path: str | None = Field(None, description="Host file path")
device_path: str | None = Field(None, description="Device file path")
output: str | None = Field(None, description="ADB output")
class FileListResult(ADBResult):
"""Directory listing result."""
path: str | None = Field(None, description="Listed directory path")
files: list[dict[str, Any]] = Field(
default_factory=list, description="File entries"
)
count: int = Field(default=0, description="Number of files")
class FileDeleteResult(ActionResult):
"""File deletion result."""
path: str | None = Field(None, description="Deleted file path")
cancelled: bool | None = Field(None, description="Whether cancelled")
message: str | None = Field(None, description="Status message")
class FileExistsResult(ADBResult):
"""File existence check result."""
path: str | None = Field(None, description="Checked path")
exists: bool = Field(description="Whether the file exists")
# ── Settings ─────────────────────────────────────────────────────────
class SettingGetResult(ADBResult):
"""Settings read result."""
namespace: str | None = Field(None, description="Settings namespace")
key: str | None = Field(None, description="Setting key")
value: str | None = Field(None, description="Setting value")
exists: bool | None = Field(None, description="Whether key exists")
class SettingPutResult(ADBResult):
"""Settings write result."""
namespace: str | None = Field(None, description="Settings namespace")
key: str | None = Field(None, description="Setting key")
value: str | None = Field(None, description="Value written")
readback: str | None = Field(None, description="Read-back verification")
verified: bool | None = Field(None, description="Whether verified")
cancelled: bool | None = Field(None, description="Whether cancelled")
message: str | None = Field(None, description="Status message")
class ToggleResult(ActionResult):
"""Radio/toggle result (wifi, bluetooth, airplane)."""
verified: bool | None = Field(None, description="Whether state verified")
wifi_on: str | None = Field(None, description="WiFi state after toggle")
airplane_mode: bool | None = Field(None, description="Airplane mode state")
cancelled: bool | None = Field(None, description="Whether cancelled")
message: str | None = Field(None, description="Status message")
class BrightnessResult(ADBResult):
"""Screen brightness result."""
brightness: int | None = Field(None, description="Brightness level 0-255")
auto_brightness: bool | None = Field(None, description="Auto-brightness state")
class TimeoutResult(ADBResult):
"""Screen timeout result."""
timeout_seconds: int | None = Field(None, description="Timeout in seconds")
timeout_ms: int | None = Field(None, description="Timeout in milliseconds")
class NotificationListResult(ADBResult):
"""Notification list result."""
notifications: list[dict[str, str | None]] = Field(
default_factory=list, description="Notification entries"
)
count: int = Field(default=0, description="Number of notifications")
class ClipboardGetResult(ADBResult):
"""Clipboard read result."""
text: str | None = Field(None, description="Clipboard text content")
method: str | None = Field(None, description="Method used to read")
class MediaControlResult(ActionResult):
"""Media control result."""
keycode: str | None = Field(None, description="Android keycode sent")
# ── Config ───────────────────────────────────────────────────────────
class ConfigStatusResult(BaseModel):
"""Server configuration status."""
developer_mode: bool = Field(description="Developer mode enabled")
auto_select_single_device: bool = Field(description="Auto-select when one device")
default_screenshot_dir: str | None = Field(
None, description="Screenshot output directory"
)
current_device: str | None = Field(None, description="Currently selected device")
class ConfigResult(ADBResult):
"""Configuration change result."""
developer_mode: bool | None = Field(None, description="Developer mode state")
screenshot_dir: str | None = Field(None, description="Screenshot directory")
message: str | None = Field(None, description="Status message")