quash-mcp 0.2.2__tar.gz → 0.2.4__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.

Files changed (25) hide show
  1. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/PKG-INFO +1 -1
  2. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/pyproject.toml +1 -1
  3. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/backend_client.py +91 -0
  4. quash_mcp-0.2.4/quash_mcp/device/state_capture.py +115 -0
  5. quash_mcp-0.2.4/quash_mcp/tools/execute.py +31 -0
  6. quash_mcp-0.2.4/quash_mcp/tools/execute_v3.py +306 -0
  7. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/.gitignore +0 -0
  8. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/README.md +0 -0
  9. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/SETUP_CLAUDE_CODE.md +0 -0
  10. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/__init__.py +0 -0
  11. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/__main__.py +0 -0
  12. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/device/__init__.py +0 -0
  13. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/device/adb_tools.py +0 -0
  14. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/device/portal.py +0 -0
  15. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/server.py +0 -0
  16. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/state.py +0 -0
  17. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/__init__.py +0 -0
  18. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/build.py +0 -0
  19. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/build_old.py +0 -0
  20. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/configure.py +0 -0
  21. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/connect.py +0 -0
  22. /quash_mcp-0.2.2/quash_mcp/tools/execute.py → /quash_mcp-0.2.4/quash_mcp/tools/execute_v2_backup.py +0 -0
  23. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/runsuite.py +0 -0
  24. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/usage.py +0 -0
  25. {quash_mcp-0.2.2 → quash_mcp-0.2.4}/test_backend_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quash-mcp
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quash-mcp"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Model Context Protocol server for Quash - AI-powered mobile automation agent"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -190,6 +190,97 @@ class BackendClient:
190
190
  logger.error(f"Failed to log execution: {e}")
191
191
  return {"logged": False, "error": str(e)}
192
192
 
193
+ async def execute_step(
194
+ self,
195
+ api_key: str,
196
+ session_id: str,
197
+ step_number: int,
198
+ task: str,
199
+ ui_state: Dict[str, Any],
200
+ chat_history: list,
201
+ config: Dict[str, Any],
202
+ screenshot_bytes: Optional[bytes] = None
203
+ ) -> Dict[str, Any]:
204
+ """
205
+ Execute single agent step (V3 - Step-by-step execution).
206
+
207
+ Sends device state to backend, receives next action to execute.
208
+
209
+ Args:
210
+ api_key: Quash API key
211
+ session_id: Unique session identifier
212
+ step_number: Current step number
213
+ task: Original task description
214
+ ui_state: Device UI state (a11y_tree, phone_state)
215
+ chat_history: Previous conversation messages
216
+ config: Execution configuration
217
+ screenshot_bytes: Optional screenshot (only if vision=True)
218
+
219
+ Returns:
220
+ Dict with action to execute:
221
+ {
222
+ "action": {"type": str, "code": str, "reasoning": str},
223
+ "completed": bool,
224
+ "success": bool (if completed),
225
+ "final_message": str (if completed),
226
+ "assistant_response": str,
227
+ "tokens_used": {"prompt": int, "completion": int, "total": int},
228
+ "cost": float
229
+ }
230
+ """
231
+ import json
232
+
233
+ try:
234
+ # Prepare form data (multipart)
235
+ data_dict = {
236
+ "api_key": api_key,
237
+ "session_id": session_id,
238
+ "step_number": step_number,
239
+ "task": task,
240
+ "ui_state": ui_state,
241
+ "chat_history": chat_history,
242
+ "config": config
243
+ }
244
+
245
+ # Convert to JSON string
246
+ data_json = json.dumps(data_dict)
247
+
248
+ # Prepare form data (data field as string)
249
+ form_data = {"data": data_json}
250
+
251
+ # Prepare files dict (only screenshot if provided)
252
+ files = {}
253
+ if screenshot_bytes:
254
+ files["screenshot"] = ("screenshot.png", screenshot_bytes, "image/png")
255
+
256
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
257
+ # Send both form data and files (multipart/form-data)
258
+ response = await client.post(
259
+ f"{self.base_url}/api/agent/step",
260
+ data=form_data,
261
+ files=files if files else None
262
+ )
263
+
264
+ if response.status_code == 200:
265
+ return response.json()
266
+ else:
267
+ error_msg = f"Backend error: HTTP {response.status_code}"
268
+ logger.error(error_msg)
269
+ return {
270
+ "status": "error",
271
+ "message": error_msg,
272
+ "error": error_msg
273
+ }
274
+
275
+ except Exception as e:
276
+ error_msg = f"Failed to execute step: {str(e)}"
277
+ logger.error(error_msg)
278
+ return {
279
+ "status": "error",
280
+ "message": error_msg,
281
+ "error": str(e)
282
+ }
283
+
193
284
 
194
285
  # Singleton instance
195
286
  _backend_client = None
@@ -0,0 +1,115 @@
1
+ """
2
+ Device state capture utilities.
3
+ Captures UI state and screenshots from Android devices.
4
+ """
5
+
6
+ import logging
7
+ import requests
8
+ from typing import Dict, Any, Optional, Tuple
9
+ from adbutils import adb
10
+
11
+ logger = logging.getLogger("quash-device")
12
+
13
+
14
+ def get_current_package(serial: str) -> str:
15
+ """
16
+ Get the currently focused app package.
17
+
18
+ Args:
19
+ serial: Device serial number
20
+
21
+ Returns:
22
+ Package name of current app
23
+ """
24
+ try:
25
+ device = adb.device(serial)
26
+ output = device.shell("dumpsys window windows | grep -E 'mCurrentFocus'")
27
+ # Parse output like: mCurrentFocus=Window{abc123 u0 com.android.settings/com.android.settings.MainActivity}
28
+ if "/" in output:
29
+ package = output.split("/")[0].split()[-1]
30
+ return package
31
+ return "unknown"
32
+ except Exception as e:
33
+ logger.warning(f"Failed to get current package: {e}")
34
+ return "unknown"
35
+
36
+
37
+ def get_accessibility_tree(serial: str, tcp_port: int = 8080) -> str:
38
+ """
39
+ Get accessibility tree from Portal app via TCP.
40
+
41
+ Args:
42
+ serial: Device serial number
43
+ tcp_port: Local TCP port for Portal communication
44
+
45
+ Returns:
46
+ Accessibility tree XML string
47
+ """
48
+ try:
49
+ device = adb.device(serial)
50
+ local_port = device.forward_port(tcp_port)
51
+
52
+ response = requests.get(
53
+ f"http://localhost:{local_port}/get_a11y_tree",
54
+ timeout=10
55
+ )
56
+
57
+ if response.status_code == 200:
58
+ return response.text
59
+ else:
60
+ logger.warning(f"Failed to get accessibility tree: HTTP {response.status_code}")
61
+ return "<hierarchy></hierarchy>"
62
+
63
+ except Exception as e:
64
+ logger.warning(f"Failed to get accessibility tree: {e}")
65
+ return "<hierarchy></hierarchy>"
66
+
67
+
68
+ def capture_screenshot(serial: str) -> Optional[bytes]:
69
+ """
70
+ Capture screenshot from device.
71
+
72
+ Args:
73
+ serial: Device serial number
74
+
75
+ Returns:
76
+ Screenshot as PNG bytes, or None if failed
77
+ """
78
+ try:
79
+ device = adb.device(serial)
80
+ screenshot_bytes = device.shell("screencap -p", stream=True)
81
+ return screenshot_bytes
82
+ except Exception as e:
83
+ logger.error(f"Failed to capture screenshot: {e}")
84
+ return None
85
+
86
+
87
+ def get_device_state(serial: str) -> Tuple[Dict[str, Any], Optional[bytes]]:
88
+ """
89
+ Get complete device state: UI state and screenshot.
90
+
91
+ Args:
92
+ serial: Device serial number
93
+
94
+ Returns:
95
+ Tuple of (ui_state_dict, screenshot_bytes)
96
+ """
97
+ # Get current package
98
+ current_package = get_current_package(serial)
99
+
100
+ # Get accessibility tree
101
+ a11y_tree = get_accessibility_tree(serial)
102
+
103
+ # Build UI state
104
+ ui_state = {
105
+ "a11y_tree": a11y_tree,
106
+ "phone_state": {
107
+ "package": current_package,
108
+ "activity": "unknown", # Can be added later
109
+ }
110
+ }
111
+
112
+ # Capture screenshot
113
+ screenshot = capture_screenshot(serial)
114
+
115
+ return ui_state, screenshot
@@ -0,0 +1,31 @@
1
+ """
2
+ Execute tool - Run automation tasks via step-by-step backend communication.
3
+
4
+ V3: Hybrid architecture - AI logic on backend (private), device access local (public).
5
+ """
6
+
7
+ from typing import Dict, Any, Callable, Optional
8
+ from .execute_v3 import execute_v3
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
+ Uses step-by-step execution:
19
+ - Captures device state locally
20
+ - Sends to backend for AI decision
21
+ - Executes actions locally
22
+ - Keeps proprietary AI logic private on backend
23
+
24
+ Args:
25
+ task: Natural language task description
26
+ progress_callback: Optional callback for progress updates
27
+
28
+ Returns:
29
+ Dict with execution result and details
30
+ """
31
+ return await execute_v3(task=task, progress_callback=progress_callback)
@@ -0,0 +1,306 @@
1
+ """
2
+ Execute tool V3 - Step-by-step execution with local device access.
3
+
4
+ AI logic runs on backend (private), device access happens locally (public).
5
+ This hybrid approach keeps proprietary code private while allowing local device control.
6
+ """
7
+
8
+ import time
9
+ import uuid
10
+ from typing import Dict, Any, Callable, Optional
11
+ from ..state import get_state
12
+ from ..backend_client import get_backend_client
13
+ from ..device.state_capture import get_device_state
14
+ from ..device.adb_tools import AdbTools
15
+
16
+
17
+ async def execute_v3(
18
+ task: str,
19
+ progress_callback: Optional[Callable[[str], None]] = None
20
+ ) -> Dict[str, Any]:
21
+ """
22
+ Execute automation task using step-by-step backend communication.
23
+
24
+ Each step:
25
+ 1. Capture device state locally (UI + optional screenshot)
26
+ 2. Send to backend for AI decision
27
+ 3. Execute returned action locally
28
+ 4. Repeat until complete
29
+
30
+ Args:
31
+ task: Natural language task description
32
+ progress_callback: Optional callback for progress updates
33
+
34
+ Returns:
35
+ Dict with execution result and details
36
+ """
37
+ state = get_state()
38
+ backend = get_backend_client()
39
+
40
+ # Check prerequisites
41
+ if not state.is_device_connected():
42
+ return {
43
+ "status": "error",
44
+ "message": "❌ No device connected. Please run 'connect' first.",
45
+ "prerequisite": "connect"
46
+ }
47
+
48
+ if not state.is_configured():
49
+ return {
50
+ "status": "error",
51
+ "message": "❌ Configuration incomplete. Please run 'configure' with your Quash API key.",
52
+ "prerequisite": "configure"
53
+ }
54
+
55
+ if not state.portal_ready:
56
+ return {
57
+ "status": "error",
58
+ "message": "⚠️ Portal accessibility service not ready. Please ensure it's enabled on the device.",
59
+ "prerequisite": "connect"
60
+ }
61
+
62
+ # Get API key and config
63
+ quash_api_key = state.config["api_key"]
64
+ config = {
65
+ "model": state.config["model"],
66
+ "temperature": state.config["temperature"],
67
+ "vision": state.config["vision"],
68
+ "reasoning": state.config["reasoning"],
69
+ "reflection": state.config["reflection"],
70
+ "debug": state.config["debug"]
71
+ }
72
+
73
+ # Validate API key
74
+ validation_result = await backend.validate_api_key(quash_api_key)
75
+
76
+ if not validation_result.get("valid", False):
77
+ error_msg = validation_result.get("error", "Invalid API key")
78
+ return {
79
+ "status": "error",
80
+ "message": f"❌ API Key validation failed: {error_msg}",
81
+ "prerequisite": "configure"
82
+ }
83
+
84
+ # Check credits
85
+ user_info = validation_result.get("user", {})
86
+ credits = user_info.get("credits", 0)
87
+
88
+ if credits <= 0:
89
+ return {
90
+ "status": "error",
91
+ "message": f"❌ Insufficient credits. Current balance: ${credits:.2f}",
92
+ "user": user_info
93
+ }
94
+
95
+ # Progress logging helper
96
+ def log_progress(message: str):
97
+ if progress_callback:
98
+ progress_callback(message)
99
+
100
+ log_progress(f"✅ API Key validated - Credits: ${credits:.2f}")
101
+ log_progress(f"👤 User: {user_info.get('name', 'Unknown')}")
102
+ log_progress(f"🚀 Starting task: {task}")
103
+ log_progress(f"📱 Device: {state.device_serial}")
104
+ log_progress(f"🧠 Model: {config['model']}")
105
+
106
+ # Initialize execution
107
+ start_time = time.time()
108
+ session_id = f"session_{uuid.uuid4().hex[:12]}"
109
+ step_number = 0
110
+ chat_history = []
111
+ total_tokens = {"prompt": 0, "completion": 0, "total": 0}
112
+ total_cost = 0.0
113
+
114
+ # Initialize local ADB tools for code execution
115
+ adb_tools = AdbTools(serial=state.device_serial, use_tcp=True)
116
+
117
+ # Code executor namespace
118
+ executor_globals = {
119
+ "__builtins__": __builtins__,
120
+ "adb_tools": adb_tools
121
+ }
122
+ executor_locals = {}
123
+
124
+ try:
125
+ # ============================================================
126
+ # STEP-BY-STEP EXECUTION LOOP
127
+ # ============================================================
128
+ while step_number < 15: # Max 15 steps
129
+ step_number += 1
130
+ log_progress(f"🧠 Step {step_number}: Thinking...")
131
+
132
+ # 1. Capture device state
133
+ try:
134
+ ui_state_dict, screenshot_bytes = get_device_state(state.device_serial)
135
+
136
+ # Only include screenshot if vision is enabled
137
+ if not config["vision"]:
138
+ screenshot_bytes = None
139
+
140
+ except Exception as e:
141
+ log_progress(f"⚠️ Warning: Failed to capture device state: {e}")
142
+ ui_state_dict = {
143
+ "a11y_tree": "<hierarchy></hierarchy>",
144
+ "phone_state": {"package": "unknown"}
145
+ }
146
+ screenshot_bytes = None
147
+
148
+ # 2. Send to backend for AI decision
149
+ step_result = await backend.execute_step(
150
+ api_key=quash_api_key,
151
+ session_id=session_id,
152
+ step_number=step_number,
153
+ task=task,
154
+ ui_state=ui_state_dict,
155
+ chat_history=chat_history,
156
+ config=config,
157
+ screenshot_bytes=screenshot_bytes
158
+ )
159
+
160
+ # Handle backend errors
161
+ if "error" in step_result:
162
+ log_progress(f"💥 Backend error: {step_result['message']}")
163
+ return {
164
+ "status": "error",
165
+ "message": step_result["message"],
166
+ "error": step_result["error"],
167
+ "steps_taken": step_number,
168
+ "tokens": total_tokens,
169
+ "cost": total_cost,
170
+ "duration_seconds": time.time() - start_time
171
+ }
172
+
173
+ # Update usage tracking
174
+ step_tokens = step_result.get("tokens_used", {})
175
+ step_cost = step_result.get("cost", 0.0)
176
+
177
+ total_tokens["prompt"] += step_tokens.get("prompt", 0)
178
+ total_tokens["completion"] += step_tokens.get("completion", 0)
179
+ total_tokens["total"] += step_tokens.get("total", 0)
180
+ total_cost += step_cost
181
+
182
+ # Get action from backend
183
+ action = step_result.get("action", {})
184
+ action_type = action.get("type")
185
+ code = action.get("code")
186
+ reasoning = action.get("reasoning")
187
+
188
+ # Log reasoning
189
+ if reasoning:
190
+ log_progress(f"🤔 Reasoning: {reasoning}")
191
+
192
+ # Update chat history
193
+ assistant_response = step_result.get("assistant_response", "")
194
+ chat_history.append({"role": "assistant", "content": assistant_response})
195
+
196
+ # 3. Check if task is complete
197
+ if step_result.get("completed", False):
198
+ success = step_result.get("success", False)
199
+ final_message = step_result.get("final_message", "Task completed")
200
+
201
+ duration = time.time() - start_time
202
+
203
+ if success:
204
+ log_progress(f"✅ Task completed successfully in {step_number} steps")
205
+ log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
206
+
207
+ return {
208
+ "status": "success",
209
+ "steps_taken": step_number,
210
+ "final_message": final_message,
211
+ "message": f"✅ Success: {final_message}",
212
+ "tokens": total_tokens,
213
+ "cost": total_cost,
214
+ "duration_seconds": duration
215
+ }
216
+ else:
217
+ log_progress(f"❌ Task failed: {final_message}")
218
+ log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
219
+
220
+ return {
221
+ "status": "failed",
222
+ "steps_taken": step_number,
223
+ "final_message": final_message,
224
+ "message": f"❌ Failed: {final_message}",
225
+ "tokens": total_tokens,
226
+ "cost": total_cost,
227
+ "duration_seconds": duration
228
+ }
229
+
230
+ # 4. Execute action locally
231
+ if code and action_type == "execute_code":
232
+ log_progress(f"⚡ Executing action...")
233
+
234
+ try:
235
+ # Execute code in sandbox
236
+ exec(code, executor_globals, executor_locals)
237
+
238
+ # Get execution result
239
+ execution_output = executor_locals.get("_result", "Code executed successfully")
240
+
241
+ # Add execution result to chat history
242
+ chat_history.append({
243
+ "role": "user",
244
+ "content": f"Execution Result:\n```\n{execution_output}\n```"
245
+ })
246
+
247
+ except Exception as e:
248
+ error_msg = f"Error during execution: {str(e)}"
249
+ log_progress(f"💥 Action failed: {error_msg}")
250
+
251
+ # Add error to chat history
252
+ chat_history.append({
253
+ "role": "user",
254
+ "content": f"Execution Result:\n```\n{error_msg}\n```"
255
+ })
256
+
257
+ else:
258
+ # No code to execute
259
+ log_progress("⚠️ No action code provided by backend")
260
+ chat_history.append({
261
+ "role": "user",
262
+ "content": "No code was provided. Please provide code to execute."
263
+ })
264
+
265
+ # Max steps reached
266
+ log_progress(f"⚠️ Reached maximum steps ({step_number})")
267
+ log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
268
+
269
+ return {
270
+ "status": "failed",
271
+ "steps_taken": step_number,
272
+ "final_message": f"Reached maximum step limit of {step_number}",
273
+ "message": "❌ Failed: Maximum steps reached",
274
+ "tokens": total_tokens,
275
+ "cost": total_cost,
276
+ "duration_seconds": time.time() - start_time
277
+ }
278
+
279
+ except KeyboardInterrupt:
280
+ log_progress("⏹️ Task interrupted by user")
281
+ return {
282
+ "status": "interrupted",
283
+ "message": "⏹️ Task execution interrupted",
284
+ "steps_taken": step_number,
285
+ "tokens": total_tokens,
286
+ "cost": total_cost,
287
+ "duration_seconds": time.time() - start_time
288
+ }
289
+
290
+ except Exception as e:
291
+ error_msg = str(e)
292
+ log_progress(f"💥 Error: {error_msg}")
293
+ return {
294
+ "status": "error",
295
+ "message": f"💥 Execution error: {error_msg}",
296
+ "error": error_msg,
297
+ "steps_taken": step_number,
298
+ "tokens": total_tokens,
299
+ "cost": total_cost,
300
+ "duration_seconds": time.time() - start_time
301
+ }
302
+
303
+ finally:
304
+ # Cleanup TCP forwarding
305
+ if adb_tools:
306
+ adb_tools.teardown_tcp_forward()
File without changes
File without changes
File without changes
File without changes