quash-mcp 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of quash-mcp might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quash-mcp
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Model Context Protocol server for Quash - AI-powered mobile automation agent
5
5
  Project-URL: Homepage, https://quashbugs.com
6
6
  Project-URL: Repository, https://github.com/quash/quash-mcp
@@ -24,6 +24,7 @@ Requires-Dist: httpx>=0.27.0
24
24
  Requires-Dist: mcp>=0.9.0
25
25
  Requires-Dist: pydantic>=2.0.0
26
26
  Requires-Dist: python-dotenv>=1.0.0
27
+ Requires-Dist: requests>=2.31.0
27
28
  Requires-Dist: rich>=13.0.0
28
29
  Provides-Extra: dev
29
30
  Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quash-mcp"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Model Context Protocol server for Quash - AI-powered mobile automation agent"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -23,6 +23,7 @@ dependencies = [
23
23
  "rich>=13.0.0",
24
24
  "pydantic>=2.0.0",
25
25
  "python-dotenv>=1.0.0",
26
+ "requests>=2.31.0",
26
27
  "adbutils==2.10.0",
27
28
  "apkutils==2.0.0",
28
29
  ]
@@ -15,8 +15,8 @@ class BackendClient:
15
15
  """Client for communicating with Quash backend API."""
16
16
 
17
17
  def __init__(self):
18
- # Get backend URL from environment variable, default to localhost for development
19
- self.base_url = os.getenv("MAHORAGA_BACKEND_URL", "http://localhost:8000")
18
+ # Get backend URL from environment variable, default to production backend
19
+ self.base_url = os.getenv("MAHORAGA_BACKEND_URL", "http://13.220.180.140:8000")
20
20
  self.timeout = 300.0 # 5 minutes for long-running LLM calls
21
21
  logger.info(f"🔧 Backend client initialized: URL={self.base_url}")
22
22
 
@@ -0,0 +1,34 @@
1
+ """
2
+ Device management module for Quash MCP.
3
+
4
+ This module contains utilities for managing Android devices, including:
5
+ - Portal APK installation and setup
6
+ - ADB device communication
7
+ - Accessibility service management
8
+ """
9
+
10
+ from .portal import (
11
+ download_portal_apk,
12
+ get_local_portal_apk,
13
+ use_portal_apk,
14
+ enable_portal_accessibility,
15
+ check_portal_accessibility,
16
+ ping_portal,
17
+ ping_portal_content,
18
+ ping_portal_tcp,
19
+ PORTAL_PACKAGE_NAME,
20
+ A11Y_SERVICE_NAME,
21
+ )
22
+
23
+ __all__ = [
24
+ "download_portal_apk",
25
+ "get_local_portal_apk",
26
+ "use_portal_apk",
27
+ "enable_portal_accessibility",
28
+ "check_portal_accessibility",
29
+ "ping_portal",
30
+ "ping_portal_content",
31
+ "ping_portal_tcp",
32
+ "PORTAL_PACKAGE_NAME",
33
+ "A11Y_SERVICE_NAME",
34
+ ]
@@ -0,0 +1,108 @@
1
+ """
2
+ ADB Tools - Basic Android device communication wrapper.
3
+ Simplified version for device management without agent-specific functionality.
4
+ """
5
+
6
+ import logging
7
+ from typing import Optional
8
+ from adbutils import adb
9
+ import requests
10
+
11
+ logger = logging.getLogger("quash-device")
12
+ PORTAL_DEFAULT_TCP_PORT = 8080
13
+
14
+
15
+ class AdbTools:
16
+ """Basic ADB device communication wrapper."""
17
+
18
+ def __init__(
19
+ self,
20
+ serial: str | None = None,
21
+ use_tcp: bool = False,
22
+ remote_tcp_port: int = PORTAL_DEFAULT_TCP_PORT,
23
+ ) -> None:
24
+ """Initialize the AdbTools instance.
25
+
26
+ Args:
27
+ serial: Device serial number
28
+ use_tcp: Whether to use TCP communication (default: False)
29
+ remote_tcp_port: TCP port for communication (default: 8080)
30
+ """
31
+ self.device = adb.device(serial=serial)
32
+ self.use_tcp = use_tcp
33
+ self.remote_tcp_port = remote_tcp_port
34
+ self.tcp_forwarded = False
35
+
36
+ # Set up TCP forwarding if requested
37
+ if self.use_tcp:
38
+ self.setup_tcp_forward()
39
+
40
+ def setup_tcp_forward(self) -> bool:
41
+ """
42
+ Set up ADB TCP port forwarding for communication with the portal app.
43
+
44
+ Returns:
45
+ bool: True if forwarding was set up successfully, False otherwise
46
+ """
47
+ try:
48
+ logger.debug(
49
+ f"Setting up TCP port forwarding for port tcp:{self.remote_tcp_port} on device {self.device.serial}"
50
+ )
51
+ # Use adb forward command to set up port forwarding
52
+ self.local_tcp_port = self.device.forward_port(self.remote_tcp_port)
53
+ self.tcp_base_url = f"http://localhost:{self.local_tcp_port}"
54
+ logger.debug(
55
+ f"TCP port forwarding set up successfully to {self.tcp_base_url}"
56
+ )
57
+
58
+ # Test the connection with a ping
59
+ try:
60
+ response = requests.get(f"{self.tcp_base_url}/ping", timeout=5)
61
+ if response.status_code == 200:
62
+ logger.debug("TCP connection test successful")
63
+ self.tcp_forwarded = True
64
+ return True
65
+ else:
66
+ logger.warning(
67
+ f"TCP connection test failed with status: {response.status_code}"
68
+ )
69
+ return False
70
+ except requests.exceptions.RequestException as e:
71
+ logger.warning(f"TCP connection test failed: {e}")
72
+ return False
73
+
74
+ except Exception as e:
75
+ logger.error(f"Failed to set up TCP port forwarding: {e}")
76
+ self.tcp_forwarded = False
77
+ return False
78
+
79
+ def teardown_tcp_forward(self) -> bool:
80
+ """
81
+ Remove ADB TCP port forwarding.
82
+
83
+ Returns:
84
+ bool: True if forwarding was removed successfully, False otherwise
85
+ """
86
+ try:
87
+ if self.tcp_forwarded:
88
+ logger.debug(
89
+ f"Removing TCP port forwarding for port {self.local_tcp_port}"
90
+ )
91
+ # remove forwarding
92
+ cmd = f"killforward:tcp:{self.local_tcp_port}"
93
+ logger.debug(f"Removing TCP port forwarding: {cmd}")
94
+ c = self.device.open_transport(cmd)
95
+ c.close()
96
+
97
+ self.tcp_forwarded = False
98
+ logger.debug(f"TCP port forwarding removed")
99
+ return True
100
+ return True
101
+ except Exception as e:
102
+ logger.error(f"Failed to remove TCP port forwarding: {e}")
103
+ return False
104
+
105
+ def __del__(self):
106
+ """Cleanup when the object is destroyed."""
107
+ if hasattr(self, "tcp_forwarded") and self.tcp_forwarded:
108
+ self.teardown_tcp_forward()
@@ -0,0 +1,163 @@
1
+ import os
2
+ import contextlib
3
+ import tempfile
4
+ import requests
5
+ from adbutils import adb, AdbDevice
6
+ from rich.console import Console
7
+
8
+ ASSET_NAME = "mahoraga-portal"
9
+ APK_DOWNLOAD_URL = "https://storage.googleapis.com/misc_quash_static/mahoraga-portal-v0.1.apk"
10
+
11
+ PORTAL_PACKAGE_NAME = "com.mahoraga.portal"
12
+ A11Y_SERVICE_NAME = (
13
+ f"{PORTAL_PACKAGE_NAME}/com.mahoraga.portal.MahoragaAccessibilityService"
14
+ )
15
+
16
+
17
+ def download_portal_apk(debug: bool = False) -> str:
18
+ """Download the Mahoraga Portal APK from cloud storage."""
19
+ console = Console()
20
+
21
+ try:
22
+ console.print("📥 Downloading Quash Portal APK...")
23
+ if debug:
24
+ console.print(f"Download URL: {APK_DOWNLOAD_URL}")
25
+
26
+ response = requests.get(APK_DOWNLOAD_URL, stream=True)
27
+ response.raise_for_status()
28
+
29
+ # Create temporary file
30
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.apk')
31
+
32
+ # Download with progress
33
+ total_size = int(response.headers.get('content-length', 0))
34
+ downloaded = 0
35
+
36
+ for chunk in response.iter_content(chunk_size=8192):
37
+ if chunk:
38
+ temp_file.write(chunk)
39
+ downloaded += len(chunk)
40
+ if total_size > 0:
41
+ progress = (downloaded / total_size) * 100
42
+ console.print(f"\rProgress: {progress:.1f}%", end="")
43
+
44
+ temp_file.close()
45
+ console.print(f"\n✅ Downloaded APK to {temp_file.name}")
46
+ return temp_file.name
47
+
48
+ except requests.RequestException as e:
49
+ raise Exception(f"Failed to download Portal APK: {e}")
50
+ except Exception as e:
51
+ raise Exception(f"Error downloading Portal APK: {e}")
52
+
53
+
54
+ def get_local_portal_apk(apk_path: str = None):
55
+ """Get the path to a local portal APK file."""
56
+ if apk_path and os.path.exists(apk_path):
57
+ return apk_path
58
+
59
+ # Look for APK in common locations
60
+ common_paths = [
61
+ f"{ASSET_NAME}.apk",
62
+ f"./assets/{ASSET_NAME}.apk",
63
+ f"./portal/{ASSET_NAME}.apk"
64
+ ]
65
+
66
+ for path in common_paths:
67
+ if os.path.exists(path):
68
+ return path
69
+
70
+ # If no local APK found, download it
71
+ return download_portal_apk()
72
+
73
+
74
+ @contextlib.contextmanager
75
+ def use_portal_apk(apk_path: str = None, debug: bool = False):
76
+ console = Console()
77
+ temp_file = None
78
+
79
+ try:
80
+ local_apk_path = get_local_portal_apk(apk_path)
81
+ console.print(f"📱 Using Portal APK: [bold]{os.path.basename(local_apk_path)}[/bold]")
82
+ if debug:
83
+ console.print(f"APK Path: {local_apk_path}")
84
+
85
+ # Track if this is a temp file we downloaded
86
+ if local_apk_path.startswith(tempfile.gettempdir()):
87
+ temp_file = local_apk_path
88
+
89
+ yield local_apk_path
90
+ except Exception as e:
91
+ console.print(f"[red]Error: {e}[/red]")
92
+ raise
93
+ finally:
94
+ # Clean up downloaded temp file
95
+ if temp_file and os.path.exists(temp_file):
96
+ try:
97
+ os.unlink(temp_file)
98
+ if debug:
99
+ console.print(f"🗑️ Cleaned up temp file: {temp_file}")
100
+ except Exception:
101
+ pass # Ignore cleanup errors
102
+
103
+
104
+ def enable_portal_accessibility(device: AdbDevice):
105
+ device.shell(
106
+ f"settings put secure enabled_accessibility_services {A11Y_SERVICE_NAME}"
107
+ )
108
+ device.shell("settings put secure accessibility_enabled 1")
109
+
110
+
111
+ def check_portal_accessibility(device: AdbDevice, debug: bool = False) -> bool:
112
+ a11y_services = device.shell("settings get secure enabled_accessibility_services")
113
+ if not A11Y_SERVICE_NAME in a11y_services:
114
+ if debug:
115
+ print(a11y_services)
116
+ return False
117
+
118
+ a11y_enabled = device.shell("settings get secure accessibility_enabled")
119
+ if a11y_enabled != "1":
120
+ if debug:
121
+ print(a11y_enabled)
122
+ return False
123
+
124
+ return True
125
+
126
+
127
+ def ping_portal(device: AdbDevice, debug: bool = False):
128
+ """
129
+ Ping the Quash Portal to check if it is installed and accessible.
130
+ """
131
+ try:
132
+ packages = device.list_packages()
133
+ except Exception as e:
134
+ raise Exception(f"Failed to list packages: {e}")
135
+
136
+ if not PORTAL_PACKAGE_NAME in packages:
137
+ if debug:
138
+ print(packages)
139
+ raise Exception("Portal is not installed on the device")
140
+
141
+ if not check_portal_accessibility(device, debug):
142
+ device.shell("am start -a android.settings.ACCESSIBILITY_SETTINGS")
143
+ raise Exception(
144
+ "Quash Portal is not enabled as an accessibility service on the device"
145
+ )
146
+
147
+
148
+ def ping_portal_content(device: AdbDevice, debug: bool = False):
149
+ try:
150
+ state = device.shell("content query --uri content://com.mahoraga.portal/state")
151
+ if not "Row: 0 result=" in state:
152
+ raise Exception("Failed to get state from Quash Portal")
153
+ except Exception as e:
154
+ raise Exception(f"Quash Portal is not reachable: {e}")
155
+
156
+
157
+ def ping_portal_tcp(device: AdbDevice, debug: bool = False):
158
+ """Check TCP forwarding to portal."""
159
+ from .adb_tools import AdbTools
160
+ try:
161
+ tools = AdbTools(serial=device.serial, use_tcp=True)
162
+ except Exception as e:
163
+ raise Exception(f"Failed to setup TCP forwarding: {e}")
@@ -557,7 +557,7 @@ class DependencyChecker:
557
557
  def check_portal(self) -> Tuple[bool, str, str]:
558
558
  """Check if Portal APK download works"""
559
559
  try:
560
- from mahoraga.portal import download_portal_apk
560
+ from quash_mcp.device.portal import download_portal_apk
561
561
  return True, "✓ Portal APK download available", None
562
562
  except ImportError as e:
563
563
  return False, f"✗ Portal module not found: {str(e)}", None
@@ -644,40 +644,53 @@ async def build() -> Dict[str, Any]:
644
644
  all_ok = False
645
645
  print()
646
646
 
647
- # 4. Check Quash Package
648
- print("4️⃣ Checking Quash package...")
647
+ # 4. Check Quash Package (Optional - only for developers with local source)
648
+ print("4️⃣ Checking Quash package (optional for development)...")
649
649
  mhg_ok, mhg_msg, _ = checker.check_mahoraga()
650
650
  details["mahoraga"] = mhg_msg
651
651
  print(f" {mhg_msg}")
652
652
 
653
653
  mahoraga_just_installed = False
654
654
  if not mhg_ok:
655
- # Try to install
656
- print(" Attempting to install Quash...")
657
- install_ok, install_msg = checker.install_mahoraga()
658
- details["mahoraga_install"] = install_msg
659
- print(f" {install_msg}")
655
+ # Check if we're in development mode (source available)
656
+ project_root = Path(__file__).parent.parent.parent.parent
657
+ mahoraga_src = project_root / "mahoraga"
658
+
659
+ if mahoraga_src.exists() and (mahoraga_src / "pyproject.toml").exists():
660
+ # Only try to install if local source is available (development mode)
661
+ print(" Local Quash source detected. Attempting to install...")
662
+ install_ok, install_msg = checker.install_mahoraga()
663
+ details["mahoraga_install"] = install_msg
664
+ print(f" {install_msg}")
660
665
 
661
- if not install_ok:
662
- all_ok = False
666
+ if install_ok:
667
+ fixes_applied.append("Installed Quash from local source")
668
+ mahoraga_just_installed = True
663
669
  else:
664
- fixes_applied.append("Installed Quash")
665
- mahoraga_just_installed = True # Track that we just installed Quash
670
+ # Production mode - skip mahoraga (not needed, device tools in quash_mcp)
671
+ print(" ⏭️ Skipped (not required for end users)")
672
+ details["mahoraga"] = "⏭️ Skipped (device tools included in quash-mcp)"
666
673
  print()
667
674
 
668
- # 5. Check Quash Version
669
- print("5️⃣ Checking Quash version...")
670
- ver_ok, ver_msg, ver_fix = checker.check_mahoraga_version()
671
- details["mahoraga_version"] = ver_msg
672
- print(f" {ver_msg}")
673
-
674
- if not ver_ok:
675
- all_ok = False
676
- if ver_fix:
677
- fix_msg = f"Please upgrade: pip install --upgrade mahoraga"
678
- details["mahoraga_version_fix"] = fix_msg
679
- print(f" 💡 {fix_msg}")
680
- print()
675
+ # 5. Check Quash Version (only if installed)
676
+ if mhg_ok or mahoraga_just_installed:
677
+ print("5️⃣ Checking Quash version...")
678
+ ver_ok, ver_msg, ver_fix = checker.check_mahoraga_version()
679
+ details["mahoraga_version"] = ver_msg
680
+ print(f" {ver_msg}")
681
+
682
+ if not ver_ok:
683
+ # Don't fail build for version mismatch - just warn
684
+ if ver_fix:
685
+ fix_msg = f"💡 Consider upgrading: pip install --upgrade mahoraga"
686
+ details["mahoraga_version_fix"] = fix_msg
687
+ print(f" {fix_msg}")
688
+ print()
689
+ else:
690
+ print("5️⃣ Checking Quash version...")
691
+ print(" ⏭️ Skipped (Quash package not installed)")
692
+ details["mahoraga_version"] = "⏭️ Skipped"
693
+ print()
681
694
 
682
695
  # 6. Check Python Dependencies
683
696
  # Note: Only check if Quash wasn't just installed, because 'pip install -e'
@@ -41,7 +41,7 @@ def check_portal_service(serial: str) -> bool:
41
41
  """Check if Quash Portal accessibility service is enabled."""
42
42
  try:
43
43
  from adbutils import adb
44
- from mahoraga.portal import ping_portal
44
+ from quash_mcp.device.portal import ping_portal
45
45
 
46
46
  device = adb.device(serial)
47
47
  ping_portal(device, debug=False)
@@ -54,7 +54,7 @@ def setup_portal(serial: str) -> tuple[bool, str]:
54
54
  """Setup Quash Portal on the device."""
55
55
  try:
56
56
  from adbutils import adb
57
- from mahoraga.portal import use_portal_apk, enable_portal_accessibility
57
+ from quash_mcp.device.portal import use_portal_apk, enable_portal_accessibility
58
58
 
59
59
  device = adb.device(serial)
60
60
 
File without changes
File without changes
File without changes
File without changes