mcp-adb/src/config.py
Ryan Malloy 7c414f8015 Refactor to MCPMixin architecture with injection-safe shell execution
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
2026-02-10 18:30:34 -07:00

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