quash-mcp 0.2.0__py3-none-any.whl

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.

@@ -0,0 +1,185 @@
1
+ """
2
+ Build tool - Setup dependencies for Quash MCP.
3
+ Checks and installs required dependencies on the user's machine.
4
+ """
5
+
6
+ import sys
7
+ import subprocess
8
+ import shutil
9
+ import platform
10
+ from typing import Dict, Any, Tuple
11
+
12
+
13
+ def check_python_version() -> Tuple[bool, str]:
14
+ """Check if Python version is >= 3.11."""
15
+ version = sys.version_info
16
+ if version.major >= 3 and version.minor >= 11:
17
+ return True, f"✓ Python {version.major}.{version.minor}.{version.micro}"
18
+ return False, f"✗ Python {version.major}.{version.minor}.{version.micro} (requires >= 3.11)"
19
+
20
+
21
+ def check_adb() -> Tuple[bool, str]:
22
+ """Check if ADB is installed."""
23
+ if shutil.which("adb"):
24
+ try:
25
+ result = subprocess.run(
26
+ ["adb", "version"],
27
+ capture_output=True,
28
+ text=True,
29
+ timeout=5
30
+ )
31
+ version_line = result.stdout.split('\n')[0]
32
+ return True, f"✓ ADB installed ({version_line})"
33
+ except Exception:
34
+ return True, "✓ ADB installed"
35
+ return False, "✗ ADB not found"
36
+
37
+
38
+ def install_adb() -> Tuple[bool, str]:
39
+ """Attempt to install ADB based on OS."""
40
+ system = platform.system()
41
+
42
+ try:
43
+ if system == "Darwin": # macOS
44
+ # Check if Homebrew is available
45
+ if shutil.which("brew"):
46
+ subprocess.run(
47
+ ["brew", "install", "android-platform-tools"],
48
+ check=True,
49
+ capture_output=True
50
+ )
51
+ return True, "✓ ADB installed via Homebrew"
52
+ else:
53
+ return False, "✗ Homebrew not found. Install from: https://brew.sh/"
54
+
55
+ elif system == "Linux":
56
+ # Try apt-get
57
+ if shutil.which("apt-get"):
58
+ subprocess.run(
59
+ ["sudo", "apt-get", "install", "-y", "adb"],
60
+ check=True,
61
+ capture_output=True
62
+ )
63
+ return True, "✓ ADB installed via apt-get"
64
+ # Try dnf
65
+ elif shutil.which("dnf"):
66
+ subprocess.run(
67
+ ["sudo", "dnf", "install", "-y", "android-tools"],
68
+ check=True,
69
+ capture_output=True
70
+ )
71
+ return True, "✓ ADB installed via dnf"
72
+ else:
73
+ return False, "✗ Package manager not found. Install ADB manually."
74
+
75
+ elif system == "Windows":
76
+ return False, "✗ Please install ADB manually from: https://developer.android.com/tools/releases/platform-tools"
77
+
78
+ else:
79
+ return False, f"✗ Unsupported OS: {system}"
80
+
81
+ except subprocess.CalledProcessError as e:
82
+ return False, f"✗ Installation failed: {str(e)}"
83
+ except Exception as e:
84
+ return False, f"✗ Error: {str(e)}"
85
+
86
+
87
+ def check_mahoraga() -> Tuple[bool, str]:
88
+ """Check if Quash package is available."""
89
+ try:
90
+ import mahoraga
91
+ return True, "✓ Quash package ready"
92
+ except ImportError:
93
+ return False, "✗ Quash package not installed"
94
+
95
+
96
+ def install_mahoraga() -> Tuple[bool, str]:
97
+ """Install Quash package."""
98
+ try:
99
+ # Get the path to mahoraga directory
100
+ import os
101
+ mahoraga_path = os.path.join(
102
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))),
103
+ "mahoraga"
104
+ )
105
+
106
+ if os.path.exists(mahoraga_path):
107
+ subprocess.run(
108
+ [sys.executable, "-m", "pip", "install", "-e", mahoraga_path],
109
+ check=True,
110
+ capture_output=True
111
+ )
112
+ return True, "✓ Quash installed successfully"
113
+ else:
114
+ return False, f"✗ Quash directory not found at: {mahoraga_path}"
115
+ except subprocess.CalledProcessError as e:
116
+ return False, f"✗ Installation failed: {e.stderr.decode() if e.stderr else str(e)}"
117
+ except Exception as e:
118
+ return False, f"✗ Error: {str(e)}"
119
+
120
+
121
+ async def build() -> Dict[str, Any]:
122
+ """
123
+ Setup and verify all dependencies required for Quash.
124
+ Auto-installs missing dependencies where possible.
125
+
126
+ Returns:
127
+ Dict with status and details of all dependencies
128
+ """
129
+ details = {}
130
+ all_ok = True
131
+
132
+ # Check Python version
133
+ python_ok, python_msg = check_python_version()
134
+ details["python"] = python_msg
135
+ if not python_ok:
136
+ all_ok = False
137
+
138
+ # Check and install ADB
139
+ adb_ok, adb_msg = check_adb()
140
+ if not adb_ok:
141
+ # Try to auto-install
142
+ install_ok, install_msg = install_adb()
143
+ details["adb"] = install_msg
144
+ if not install_ok:
145
+ all_ok = False
146
+ else:
147
+ details["adb"] = adb_msg
148
+
149
+ # Check and install Quash
150
+ mahoraga_ok, mahoraga_msg = check_mahoraga()
151
+ if not mahoraga_ok:
152
+ # Try to auto-install
153
+ install_ok, install_msg = install_mahoraga()
154
+ details["mahoraga"] = install_msg
155
+ if not install_ok:
156
+ all_ok = False
157
+ else:
158
+ details["mahoraga"] = mahoraga_msg
159
+
160
+ # Check portal APK (just verify it exists in mahoraga)
161
+ try:
162
+ from mahoraga.portal import use_portal_apk
163
+ details["portal_apk"] = "✓ Portal APK available"
164
+ except Exception as e:
165
+ details["portal_apk"] = f"✗ Portal APK not found: {str(e)}"
166
+ all_ok = False
167
+
168
+ # Determine overall status
169
+ if all_ok:
170
+ status = "success"
171
+ message = "✅ All dependencies ready! You can now use Quash."
172
+ else:
173
+ failed_items = [k for k, v in details.items() if v.startswith("✗")]
174
+ if len(failed_items) == len(details):
175
+ status = "failed"
176
+ message = f"❌ Setup failed. Missing: {', '.join(failed_items)}"
177
+ else:
178
+ status = "partial"
179
+ message = f"⚠️ Partially ready. Issues with: {', '.join(failed_items)}"
180
+
181
+ return {
182
+ "status": status,
183
+ "details": details,
184
+ "message": message
185
+ }
@@ -0,0 +1,140 @@
1
+ """
2
+ Configure tool - Manage agent configuration parameters.
3
+ Allows users to set and update Quash agent execution parameters.
4
+ """
5
+
6
+ from typing import Dict, Any, Optional
7
+ from ..state import get_state
8
+
9
+
10
+ # Valid configuration parameters and their types
11
+ VALID_PARAMS = {
12
+ "quash_api_key": str,
13
+ "model": str,
14
+ "temperature": float,
15
+ "max_steps": int,
16
+ "vision": bool,
17
+ "reasoning": bool,
18
+ "reflection": bool,
19
+ "debug": bool,
20
+ }
21
+
22
+
23
+ def validate_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
24
+ """
25
+ Validate configuration parameters.
26
+
27
+ Returns:
28
+ (is_valid, error_message)
29
+ """
30
+ for key, value in config.items():
31
+ if key not in VALID_PARAMS:
32
+ return False, f"Invalid parameter: '{key}'. Valid parameters are: {', '.join(VALID_PARAMS.keys())}"
33
+
34
+ expected_type = VALID_PARAMS[key]
35
+ if not isinstance(value, expected_type):
36
+ return False, f"Parameter '{key}' must be of type {expected_type.__name__}, got {type(value).__name__}"
37
+
38
+ # Validate specific constraints
39
+ if "temperature" in config:
40
+ temp = config["temperature"]
41
+ if not 0 <= temp <= 2:
42
+ return False, "temperature must be between 0 and 2"
43
+
44
+ if "max_steps" in config:
45
+ steps = config["max_steps"]
46
+ if steps < 1:
47
+ return False, "max_steps must be at least 1"
48
+
49
+ if "model" in config:
50
+ model = config["model"]
51
+ # Basic model name validation
52
+ if not model or not isinstance(model, str) or len(model) < 3:
53
+ return False, "Invalid model name"
54
+
55
+ return True, None
56
+
57
+
58
+ async def configure(
59
+ quash_api_key: Optional[str] = None,
60
+ model: Optional[str] = None,
61
+ temperature: Optional[float] = None,
62
+ max_steps: Optional[int] = None,
63
+ vision: Optional[bool] = None,
64
+ reasoning: Optional[bool] = None,
65
+ reflection: Optional[bool] = None,
66
+ debug: Optional[bool] = None,
67
+ ) -> Dict[str, Any]:
68
+ """
69
+ Configure agent execution parameters.
70
+ Only updates parameters that are provided (not None).
71
+
72
+ Args:
73
+ quash_api_key: Quash API key for authentication and access
74
+ model: LLM model name (e.g., "openai/gpt-4o")
75
+ temperature: Temperature for LLM (0-2)
76
+ max_steps: Maximum number of execution steps
77
+ vision: Enable vision capabilities (screenshots)
78
+ reasoning: Enable planning with reasoning
79
+ reflection: Enable reflection for self-improvement
80
+ debug: Enable verbose debug logging
81
+
82
+ Returns:
83
+ Dict with configuration status and current settings
84
+ """
85
+ state = get_state()
86
+
87
+ # Collect provided parameters
88
+ updates = {}
89
+ if quash_api_key is not None:
90
+ updates["api_key"] = quash_api_key
91
+ if model is not None:
92
+ updates["model"] = model
93
+ if temperature is not None:
94
+ updates["temperature"] = temperature
95
+ if max_steps is not None:
96
+ updates["max_steps"] = max_steps
97
+ if vision is not None:
98
+ updates["vision"] = vision
99
+ if reasoning is not None:
100
+ updates["reasoning"] = reasoning
101
+ if reflection is not None:
102
+ updates["reflection"] = reflection
103
+ if debug is not None:
104
+ updates["debug"] = debug
105
+
106
+ # If no updates provided, just return current config
107
+ if not updates:
108
+ return {
109
+ "status": "no_changes",
110
+ "current_config": state.get_config_summary(),
111
+ "message": "ℹ️ No parameters provided. Current configuration unchanged."
112
+ }
113
+
114
+ # Validate updates (map api_key back to quash_api_key for validation)
115
+ validation_updates = updates.copy()
116
+ if "api_key" in validation_updates:
117
+ validation_updates["quash_api_key"] = validation_updates.pop("api_key")
118
+
119
+ is_valid, error_msg = validate_config(validation_updates)
120
+ if not is_valid:
121
+ return {
122
+ "status": "error",
123
+ "message": f"❌ Configuration error: {error_msg}",
124
+ "current_config": state.get_config_summary()
125
+ }
126
+
127
+ # Apply updates
128
+ state.update_config(**updates)
129
+
130
+ # Prepare response
131
+ updated_keys = list(updates.keys())
132
+ if "api_key" in updated_keys:
133
+ updated_keys[updated_keys.index("api_key")] = "quash_api_key"
134
+
135
+ return {
136
+ "status": "configured",
137
+ "updated_parameters": updated_keys,
138
+ "current_config": state.get_config_summary(),
139
+ "message": f"✅ Configuration updated: {', '.join(updated_keys)}"
140
+ }
@@ -0,0 +1,153 @@
1
+ """
2
+ Connect tool - Manage Android device connectivity.
3
+ Connects to Android devices/emulators and verifies accessibility service.
4
+ """
5
+
6
+ import subprocess
7
+ from typing import Dict, Any, Optional
8
+ from ..state import get_state
9
+
10
+
11
+ def list_devices() -> list:
12
+ """List all connected Android devices."""
13
+ try:
14
+ from adbutils import adb
15
+ devices = adb.list()
16
+ return [{"serial": d.serial, "state": d.state} for d in devices]
17
+ except Exception as e:
18
+ return []
19
+
20
+
21
+ def get_device_info(serial: str) -> Optional[Dict[str, str]]:
22
+ """Get detailed information about a device."""
23
+ try:
24
+ from adbutils import adb
25
+ device = adb.device(serial)
26
+
27
+ # Get device properties
28
+ model = device.prop.get("ro.product.model", "Unknown")
29
+ android_version = device.prop.get("ro.build.version.release", "Unknown")
30
+
31
+ return {
32
+ "serial": serial,
33
+ "model": model,
34
+ "android_version": android_version
35
+ }
36
+ except Exception as e:
37
+ return None
38
+
39
+
40
+ def check_portal_service(serial: str) -> bool:
41
+ """Check if Quash Portal accessibility service is enabled."""
42
+ try:
43
+ from adbutils import adb
44
+ from mahoraga.portal import ping_portal
45
+
46
+ device = adb.device(serial)
47
+ ping_portal(device, debug=False)
48
+ return True
49
+ except Exception:
50
+ return False
51
+
52
+
53
+ def setup_portal(serial: str) -> tuple[bool, str]:
54
+ """Setup Quash Portal on the device."""
55
+ try:
56
+ from adbutils import adb
57
+ from mahoraga.portal import use_portal_apk, enable_portal_accessibility
58
+
59
+ device = adb.device(serial)
60
+
61
+ # Install APK
62
+ with use_portal_apk(None, debug=False) as apk_path:
63
+ device.install(apk_path, uninstall=True, flags=["-g"], silent=True)
64
+
65
+ # Enable accessibility service
66
+ enable_portal_accessibility(device)
67
+
68
+ return True, "Portal installed and enabled successfully"
69
+ except Exception as e:
70
+ return False, f"Failed to setup portal: {str(e)}"
71
+
72
+
73
+ async def connect(device_serial: Optional[str] = None) -> Dict[str, Any]:
74
+ """
75
+ Connect to an Android device or emulator.
76
+ If device_serial is not provided, auto-selects if only one device is connected.
77
+
78
+ Args:
79
+ device_serial: Optional device serial number
80
+
81
+ Returns:
82
+ Dict with connection status and device information
83
+ """
84
+ state = get_state()
85
+
86
+ # List available devices
87
+ devices = list_devices()
88
+
89
+ if not devices:
90
+ return {
91
+ "status": "failed",
92
+ "message": "❌ No Android devices found. Please connect a device or start an emulator.",
93
+ "instructions": [
94
+ "To start an emulator: Open Android Studio > AVD Manager > Start",
95
+ "To connect a physical device: Enable USB debugging and connect via USB",
96
+ "To connect over WiFi: Run 'adb tcpip 5555' then 'adb connect <device-ip>:5555'"
97
+ ]
98
+ }
99
+
100
+ # Select device
101
+ selected_serial = device_serial
102
+ if not selected_serial:
103
+ if len(devices) == 1:
104
+ selected_serial = devices[0]["serial"]
105
+ else:
106
+ return {
107
+ "status": "failed",
108
+ "message": f"❌ Multiple devices found ({len(devices)}). Please specify which one to use.",
109
+ "available_devices": devices
110
+ }
111
+
112
+ # Verify device exists
113
+ if not any(d["serial"] == selected_serial for d in devices):
114
+ return {
115
+ "status": "failed",
116
+ "message": f"❌ Device '{selected_serial}' not found.",
117
+ "available_devices": devices
118
+ }
119
+
120
+ # Get device info
121
+ device_info = get_device_info(selected_serial)
122
+ if not device_info:
123
+ return {
124
+ "status": "failed",
125
+ "message": f"❌ Failed to get information for device '{selected_serial}'."
126
+ }
127
+
128
+ # Check portal accessibility service
129
+ portal_ready = check_portal_service(selected_serial)
130
+
131
+ if not portal_ready:
132
+ # Attempt to setup portal
133
+ setup_success, setup_msg = setup_portal(selected_serial)
134
+ if setup_success:
135
+ portal_ready = True
136
+ portal_message = "✓ Portal setup completed"
137
+ else:
138
+ portal_message = f"⚠️ Portal not ready: {setup_msg}"
139
+ else:
140
+ portal_message = "✓ Portal already enabled"
141
+
142
+ # Update state
143
+ state.device_serial = selected_serial
144
+ state.device_info = device_info
145
+ state.portal_ready = portal_ready
146
+
147
+ return {
148
+ "status": "connected" if portal_ready else "partial",
149
+ "device": device_info,
150
+ "portal_ready": portal_ready,
151
+ "portal_message": portal_message,
152
+ "message": f"✅ Connected to {device_info['model']} ({selected_serial})"
153
+ }
@@ -0,0 +1,177 @@
1
+ """
2
+ Execute tool - Run automation tasks via backend API.
3
+ All AI/agent logic runs on the backend to protect business logic.
4
+ """
5
+
6
+ from typing import Dict, Any, Callable, Optional
7
+ from ..state import get_state
8
+ from ..backend_client import get_backend_client
9
+
10
+
11
+ async def execute(
12
+ task: str,
13
+ progress_callback: Optional[Callable[[str], None]] = None
14
+ ) -> Dict[str, Any]:
15
+ """
16
+ Execute an automation task on the connected Android device.
17
+
18
+ All AI execution happens on the backend - this keeps proprietary logic private.
19
+
20
+ Args:
21
+ task: Natural language task description
22
+ progress_callback: Optional callback for progress updates (not used in V2)
23
+
24
+ Returns:
25
+ Dict with execution result and details
26
+ """
27
+ state = get_state()
28
+ backend = get_backend_client()
29
+
30
+ # Check prerequisites
31
+ if not state.is_device_connected():
32
+ return {
33
+ "status": "error",
34
+ "message": "❌ No device connected. Please run 'connect' first.",
35
+ "prerequisite": "connect"
36
+ }
37
+
38
+ if not state.is_configured():
39
+ return {
40
+ "status": "error",
41
+ "message": "❌ Configuration incomplete. Please run 'configure' with your Quash API key.",
42
+ "prerequisite": "configure"
43
+ }
44
+
45
+ if not state.portal_ready:
46
+ return {
47
+ "status": "error",
48
+ "message": "⚠️ Portal accessibility service not ready. Please ensure it's enabled on the device.",
49
+ "prerequisite": "connect"
50
+ }
51
+
52
+ # Get API key and config from state
53
+ quash_api_key = state.config["api_key"]
54
+
55
+ # Validate API key with backend
56
+ validation_result = await backend.validate_api_key(quash_api_key)
57
+
58
+ if not validation_result.get("valid", False):
59
+ error_msg = validation_result.get("error", "Invalid API key")
60
+ return {
61
+ "status": "error",
62
+ "message": f"❌ API Key validation failed: {error_msg}",
63
+ "prerequisite": "configure"
64
+ }
65
+
66
+ # Check user credits
67
+ user_info = validation_result.get("user", {})
68
+ credits = user_info.get("credits", 0)
69
+
70
+ if credits <= 0:
71
+ return {
72
+ "status": "error",
73
+ "message": f"❌ Insufficient credits. Current balance: ${credits:.2f}. Please add credits at https://quashbugs.com",
74
+ "user": user_info
75
+ }
76
+
77
+ # Progress callback (for backward compatibility)
78
+ def log_progress(message: str):
79
+ """Send progress updates."""
80
+ if progress_callback:
81
+ progress_callback(message)
82
+
83
+ log_progress(f"✅ API Key validated - Credits: ${credits:.2f}")
84
+ log_progress(f"👤 User: {user_info.get('name', 'Unknown')}")
85
+ log_progress(f"🚀 Starting task: {task}")
86
+ log_progress(f"📱 Device: {state.device_serial}")
87
+ log_progress(f"🧠 Model: {state.config['model']}")
88
+ log_progress("⚙️ Executing on backend...")
89
+
90
+ try:
91
+ # ============================================================
92
+ # EXECUTE ON BACKEND - ALL AI LOGIC IS PRIVATE
93
+ # The backend handles: LLM setup, agent initialization,
94
+ # execution, pricing, usage tracking, credit deduction
95
+ # ============================================================
96
+
97
+ result = await backend.execute_task(
98
+ api_key=quash_api_key,
99
+ task=task,
100
+ device_serial=state.device_serial,
101
+ config={
102
+ "model": state.config["model"],
103
+ "temperature": state.config["temperature"],
104
+ "vision": state.config["vision"],
105
+ "reasoning": state.config["reasoning"],
106
+ "reflection": state.config["reflection"],
107
+ "debug": state.config["debug"]
108
+ }
109
+ )
110
+
111
+ # Process result
112
+ status = result.get("status")
113
+ message = result.get("message", "")
114
+ steps_taken = result.get("steps_taken", 0)
115
+ final_message = result.get("final_message", "")
116
+ tokens = result.get("tokens", {})
117
+ cost = result.get("cost", 0.0)
118
+ duration = result.get("duration_seconds", 0.0)
119
+ error = result.get("error")
120
+
121
+ # Log usage info
122
+ if tokens and cost:
123
+ total_tokens = tokens.get("total", 0)
124
+ log_progress(f"💰 Usage: {total_tokens} tokens, ${cost:.4f}")
125
+
126
+ # Return formatted result
127
+ if status == "success":
128
+ log_progress(f"✅ Task completed successfully in {steps_taken} steps")
129
+ return {
130
+ "status": "success",
131
+ "steps_taken": steps_taken,
132
+ "final_message": final_message,
133
+ "message": message,
134
+ "tokens": tokens,
135
+ "cost": cost,
136
+ "duration_seconds": duration
137
+ }
138
+ elif status == "failed":
139
+ log_progress(f"❌ Task failed: {final_message}")
140
+ return {
141
+ "status": "failed",
142
+ "steps_taken": steps_taken,
143
+ "final_message": final_message,
144
+ "message": message,
145
+ "tokens": tokens,
146
+ "cost": cost,
147
+ "duration_seconds": duration
148
+ }
149
+ elif status == "interrupted":
150
+ log_progress("⏹️ Task interrupted")
151
+ return {
152
+ "status": "interrupted",
153
+ "message": message
154
+ }
155
+ else: # error
156
+ log_progress(f"💥 Error: {error or message}")
157
+ return {
158
+ "status": "error",
159
+ "message": message,
160
+ "error": error or message
161
+ }
162
+
163
+ except KeyboardInterrupt:
164
+ log_progress("⏹️ Task interrupted by user")
165
+ return {
166
+ "status": "interrupted",
167
+ "message": "⏹️ Task execution interrupted"
168
+ }
169
+
170
+ except Exception as e:
171
+ error_msg = str(e)
172
+ log_progress(f"💥 Error: {error_msg}")
173
+ return {
174
+ "status": "error",
175
+ "message": f"💥 Execution error: {error_msg}",
176
+ "error": error_msg
177
+ }