Replaces single-file server with modular mixin architecture: - 6 domain mixins (devices, input, apps, screenshot, ui, files) - Injection-safe run_shell_args() using shlex.quote() for all tools - Persistent developer mode config (~/.config/adb-mcp/config.json) - Pydantic models for typed responses - MCP elicitation for destructive operations - Dynamic screen dimensions for scroll gestures - Intent flag name resolution for activity_start - 50 tools, 5 resources, tested on real hardware
101 lines
3.0 KiB
Python
101 lines
3.0 KiB
Python
"""Configuration management for Android ADB MCP Server.
|
|
|
|
Supports developer mode and persistent settings.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
# Config file location
|
|
_default_config_dir = Path.home() / ".config" / "adb-mcp"
|
|
CONFIG_DIR = Path(os.environ.get("ADB_MCP_CONFIG_DIR", _default_config_dir))
|
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
|
|
|
|
class Config:
|
|
"""Singleton configuration manager with persistence."""
|
|
|
|
_instance: Optional["Config"] = None
|
|
_settings: dict[str, Any]
|
|
|
|
def __new__(cls) -> "Config":
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._settings = cls._instance._load()
|
|
return cls._instance
|
|
|
|
def _load(self) -> dict[str, Any]:
|
|
"""Load settings from disk."""
|
|
if CONFIG_FILE.exists():
|
|
try:
|
|
return json.loads(CONFIG_FILE.read_text())
|
|
except (json.JSONDecodeError, OSError):
|
|
pass
|
|
return self._defaults()
|
|
|
|
def _save(self) -> None:
|
|
"""Persist settings to disk."""
|
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
CONFIG_FILE.write_text(json.dumps(self._settings, indent=2))
|
|
|
|
@staticmethod
|
|
def _defaults() -> dict[str, Any]:
|
|
"""Default configuration values."""
|
|
return {
|
|
"developer_mode": False,
|
|
"default_screenshot_dir": None,
|
|
"auto_select_single_device": True,
|
|
}
|
|
|
|
@property
|
|
def developer_mode(self) -> bool:
|
|
"""Check if developer mode is enabled."""
|
|
return self._settings.get("developer_mode", False)
|
|
|
|
@developer_mode.setter
|
|
def developer_mode(self, value: bool) -> None:
|
|
"""Enable or disable developer mode."""
|
|
self._settings["developer_mode"] = value
|
|
self._save()
|
|
|
|
@property
|
|
def auto_select_single_device(self) -> bool:
|
|
"""Auto-select device when only one is connected."""
|
|
return self._settings.get("auto_select_single_device", True)
|
|
|
|
@property
|
|
def default_screenshot_dir(self) -> str | None:
|
|
"""Default directory for screenshots."""
|
|
return self._settings.get("default_screenshot_dir")
|
|
|
|
@default_screenshot_dir.setter
|
|
def default_screenshot_dir(self, value: str | None) -> None:
|
|
"""Set default screenshot directory."""
|
|
self._settings["default_screenshot_dir"] = value
|
|
self._save()
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""Get a config value."""
|
|
return self._settings.get(key, default)
|
|
|
|
def set(self, key: str, value: Any) -> None:
|
|
"""Set a config value and persist."""
|
|
self._settings[key] = value
|
|
self._save()
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Export all settings."""
|
|
return self._settings.copy()
|
|
|
|
|
|
def get_config() -> Config:
|
|
"""Get the singleton config instance."""
|
|
return Config()
|
|
|
|
|
|
def is_developer_mode() -> bool:
|
|
"""Quick check for developer mode status."""
|
|
return get_config().developer_mode
|