quash-mcp 0.2.8__py3-none-any.whl → 0.2.10__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.
- quash_mcp/backend_client.py +71 -0
- quash_mcp/device/state_capture.py +6 -2
- quash_mcp/tools/execute_v3.py +307 -42
- {quash_mcp-0.2.8.dist-info → quash_mcp-0.2.10.dist-info}/METADATA +19 -3
- {quash_mcp-0.2.8.dist-info → quash_mcp-0.2.10.dist-info}/RECORD +7 -7
- {quash_mcp-0.2.8.dist-info → quash_mcp-0.2.10.dist-info}/WHEEL +0 -0
- {quash_mcp-0.2.8.dist-info → quash_mcp-0.2.10.dist-info}/entry_points.txt +0 -0
quash_mcp/backend_client.py
CHANGED
|
@@ -281,6 +281,77 @@ class BackendClient:
|
|
|
281
281
|
"error": str(e)
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
async def finalize_session(
|
|
285
|
+
self,
|
|
286
|
+
api_key: str,
|
|
287
|
+
session_id: str,
|
|
288
|
+
task: str,
|
|
289
|
+
device_serial: str,
|
|
290
|
+
status: str,
|
|
291
|
+
final_message: Optional[str] = None,
|
|
292
|
+
error: Optional[str] = None,
|
|
293
|
+
duration_seconds: float = 0.0,
|
|
294
|
+
config: Optional[Dict[str, Any]] = None
|
|
295
|
+
) -> Dict[str, Any]:
|
|
296
|
+
"""
|
|
297
|
+
Finalize a session and aggregate execution record.
|
|
298
|
+
|
|
299
|
+
Called when task ends for ANY reason: normal completion, max steps, error, interrupt.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
api_key: Quash API key
|
|
303
|
+
session_id: Session identifier to finalize
|
|
304
|
+
task: Original task description
|
|
305
|
+
device_serial: Device serial number
|
|
306
|
+
status: "success", "failed", "max_steps", "error", "interrupted"
|
|
307
|
+
final_message: Final message from agent
|
|
308
|
+
error: Error message if failed
|
|
309
|
+
duration_seconds: Total execution time
|
|
310
|
+
config: Execution configuration
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Dict with finalization result:
|
|
314
|
+
{
|
|
315
|
+
"finalized": bool,
|
|
316
|
+
"execution_id": str,
|
|
317
|
+
"total_steps": int,
|
|
318
|
+
"total_tokens": {"prompt": int, "completion": int, "total": int},
|
|
319
|
+
"total_cost": float,
|
|
320
|
+
"error": str (if failed)
|
|
321
|
+
}
|
|
322
|
+
"""
|
|
323
|
+
logger.info(f"🏁 Finalizing session {session_id} - Status: {status}")
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
327
|
+
response = await client.post(
|
|
328
|
+
f"{self.base_url}/api/agent/finalize",
|
|
329
|
+
json={
|
|
330
|
+
"api_key": api_key,
|
|
331
|
+
"session_id": session_id,
|
|
332
|
+
"task": task,
|
|
333
|
+
"device_serial": device_serial,
|
|
334
|
+
"status": status,
|
|
335
|
+
"final_message": final_message,
|
|
336
|
+
"error": error,
|
|
337
|
+
"duration_seconds": duration_seconds,
|
|
338
|
+
"config": config or {}
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if response.status_code == 200:
|
|
343
|
+
result = response.json()
|
|
344
|
+
if result.get("finalized"):
|
|
345
|
+
logger.info(f"✅ Session finalized: {result.get('total_steps')} steps, ${result.get('total_cost', 0):.4f}")
|
|
346
|
+
return result
|
|
347
|
+
else:
|
|
348
|
+
logger.warning(f"Failed to finalize session: HTTP {response.status_code}")
|
|
349
|
+
return {"finalized": False, "error": f"HTTP {response.status_code}"}
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.error(f"Failed to finalize session: {e}")
|
|
353
|
+
return {"finalized": False, "error": str(e)}
|
|
354
|
+
|
|
284
355
|
|
|
285
356
|
# Singleton instance
|
|
286
357
|
_backend_client = None
|
|
@@ -55,9 +55,13 @@ def get_accessibility_tree(serial: str, tcp_port: int = 8080) -> str:
|
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
if response.status_code == 200:
|
|
58
|
-
# Portal returns JSON with
|
|
58
|
+
# Portal returns JSON with status and data fields
|
|
59
59
|
data = response.json()
|
|
60
|
-
|
|
60
|
+
if data.get("status") == "success":
|
|
61
|
+
return data.get("data", "<hierarchy></hierarchy>")
|
|
62
|
+
else:
|
|
63
|
+
logger.warning(f"Portal error: {data.get('error', 'Unknown error')}")
|
|
64
|
+
return "<hierarchy></hierarchy>"
|
|
61
65
|
else:
|
|
62
66
|
logger.warning(f"Failed to get accessibility tree: HTTP {response.status_code}")
|
|
63
67
|
return "<hierarchy></hierarchy>"
|
quash_mcp/tools/execute_v3.py
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Execute tool V3 - Step-by-step execution with
|
|
2
|
+
Execute tool V3 - Step-by-step execution with state-change verification.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
This reimplements the event-driven state verification from the original Mahoraga agent
|
|
5
|
+
using a polling-based approach suitable for the client-server architecture.
|
|
6
|
+
|
|
7
|
+
All state-change detection logic is contained in this file.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
import time
|
|
9
11
|
import uuid
|
|
10
12
|
import asyncio
|
|
11
|
-
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
from typing import Dict, Any, Callable, Optional, Tuple
|
|
12
16
|
from ..state import get_state
|
|
13
17
|
from ..backend_client import get_backend_client
|
|
14
18
|
from ..device.state_capture import get_device_state
|
|
@@ -29,21 +33,172 @@ except ImportError as e:
|
|
|
29
33
|
async_to_sync = None
|
|
30
34
|
|
|
31
35
|
|
|
36
|
+
def get_ui_state_hash(ui_state_dict: Dict[str, Any]) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Generate a stable hash of the UI state for comparison.
|
|
39
|
+
|
|
40
|
+
Uses accessibility tree structure and package name.
|
|
41
|
+
Hash will change when UI updates after an action.
|
|
42
|
+
"""
|
|
43
|
+
def normalize_tree(tree):
|
|
44
|
+
"""Extract stable elements from UI tree."""
|
|
45
|
+
if isinstance(tree, list):
|
|
46
|
+
normalized = []
|
|
47
|
+
for item in tree:
|
|
48
|
+
if isinstance(item, dict):
|
|
49
|
+
element = {
|
|
50
|
+
"className": item.get("className", ""),
|
|
51
|
+
"text": item.get("text", ""),
|
|
52
|
+
"resourceId": item.get("resourceId", ""),
|
|
53
|
+
"bounds": item.get("bounds", ""),
|
|
54
|
+
}
|
|
55
|
+
normalized.append(element)
|
|
56
|
+
|
|
57
|
+
children = item.get("children", [])
|
|
58
|
+
if children:
|
|
59
|
+
element["children"] = normalize_tree(children)
|
|
60
|
+
return normalized
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
state_repr = {
|
|
64
|
+
"package": ui_state_dict.get("phone_state", {}).get("package", ""),
|
|
65
|
+
"tree": normalize_tree(ui_state_dict.get("a11y_tree", []))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
state_json = json.dumps(state_repr, sort_keys=True)
|
|
69
|
+
return hashlib.sha256(state_json.encode()).hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_action_timeout(code: str) -> float:
|
|
73
|
+
"""
|
|
74
|
+
Determine appropriate timeout based on action type.
|
|
75
|
+
|
|
76
|
+
Returns timeout in seconds.
|
|
77
|
+
"""
|
|
78
|
+
code_lower = code.lower()
|
|
79
|
+
|
|
80
|
+
if "start_app" in code_lower:
|
|
81
|
+
return 10.0 # App launches can be slow
|
|
82
|
+
elif "tap" in code_lower or "click" in code_lower:
|
|
83
|
+
return 5.0 # Screen transitions
|
|
84
|
+
elif "swipe" in code_lower or "scroll" in code_lower:
|
|
85
|
+
return 2.0 # Scroll animations
|
|
86
|
+
elif "drag" in code_lower:
|
|
87
|
+
return 2.0
|
|
88
|
+
elif "input_text" in code_lower:
|
|
89
|
+
return 2.0 # Text input is fast
|
|
90
|
+
elif "press_back" in code_lower or "press_home" in code_lower:
|
|
91
|
+
return 3.0 # Navigation
|
|
92
|
+
elif "press_key" in code_lower:
|
|
93
|
+
return 1.0
|
|
94
|
+
else:
|
|
95
|
+
return 5.0 # Default timeout
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def wait_for_state_change(
|
|
99
|
+
get_state_func,
|
|
100
|
+
device_serial: str,
|
|
101
|
+
old_state_hash: str,
|
|
102
|
+
max_wait: float = 10.0,
|
|
103
|
+
poll_interval: float = 0.5,
|
|
104
|
+
min_wait: float = 0.3
|
|
105
|
+
) -> Tuple[Dict[str, Any], bytes, bool]:
|
|
106
|
+
"""
|
|
107
|
+
Poll device until UI state changes or timeout.
|
|
108
|
+
|
|
109
|
+
This is the core polling mechanism that replaces Mahoraga's event-driven approach.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Tuple of (ui_state_dict, screenshot_bytes, state_changed: bool)
|
|
113
|
+
"""
|
|
114
|
+
# Always wait minimum time for action to take effect
|
|
115
|
+
time.sleep(min_wait)
|
|
116
|
+
|
|
117
|
+
start_time = time.time()
|
|
118
|
+
|
|
119
|
+
while (time.time() - start_time) < max_wait:
|
|
120
|
+
# Capture current state
|
|
121
|
+
ui_state_dict, screenshot_bytes = get_state_func(device_serial)
|
|
122
|
+
current_hash = get_ui_state_hash(ui_state_dict)
|
|
123
|
+
|
|
124
|
+
# Check if state changed
|
|
125
|
+
if current_hash != old_state_hash:
|
|
126
|
+
return ui_state_dict, screenshot_bytes, True
|
|
127
|
+
|
|
128
|
+
# State hasn't changed - wait and try again
|
|
129
|
+
time.sleep(poll_interval)
|
|
130
|
+
|
|
131
|
+
# Timeout - state never changed
|
|
132
|
+
ui_state_dict, screenshot_bytes = get_state_func(device_serial)
|
|
133
|
+
return ui_state_dict, screenshot_bytes, False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def wait_for_action_effect(
|
|
137
|
+
get_state_func,
|
|
138
|
+
device_serial: str,
|
|
139
|
+
old_ui_state: Dict[str, Any],
|
|
140
|
+
executed_code: str,
|
|
141
|
+
min_wait: float = 0.3,
|
|
142
|
+
poll_interval: float = 0.5
|
|
143
|
+
) -> Tuple[Dict[str, Any], bytes, bool]:
|
|
144
|
+
"""
|
|
145
|
+
Wait for an action to take effect on the device.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Tuple of (new_ui_state_dict, screenshot_bytes, state_changed: bool)
|
|
149
|
+
"""
|
|
150
|
+
# Check if action should change UI
|
|
151
|
+
code_lower = executed_code.lower()
|
|
152
|
+
if "get_state" in code_lower or "complete(" in code_lower:
|
|
153
|
+
# Action doesn't change UI - no need to wait
|
|
154
|
+
time.sleep(0.1)
|
|
155
|
+
return get_state_func(device_serial)[0], None, False
|
|
156
|
+
|
|
157
|
+
# Get hash of old state
|
|
158
|
+
old_hash = get_ui_state_hash(old_ui_state)
|
|
159
|
+
|
|
160
|
+
# Determine timeout based on action type
|
|
161
|
+
timeout = get_action_timeout(executed_code)
|
|
162
|
+
|
|
163
|
+
# Poll until state changes
|
|
164
|
+
new_ui_state, screenshot, changed = wait_for_state_change(
|
|
165
|
+
get_state_func,
|
|
166
|
+
device_serial,
|
|
167
|
+
old_hash,
|
|
168
|
+
max_wait=timeout,
|
|
169
|
+
poll_interval=poll_interval,
|
|
170
|
+
min_wait=min_wait
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return new_ui_state, screenshot, changed
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ============================================================
|
|
177
|
+
# MAIN EXECUTION FUNCTION
|
|
178
|
+
# ============================================================
|
|
179
|
+
|
|
32
180
|
async def execute_v3(
|
|
33
181
|
task: str,
|
|
182
|
+
max_steps: int = 15,
|
|
34
183
|
progress_callback: Optional[Callable[[str], None]] = None
|
|
35
184
|
) -> Dict[str, Any]:
|
|
36
185
|
"""
|
|
37
186
|
Execute automation task using step-by-step backend communication.
|
|
38
187
|
|
|
39
188
|
Each step:
|
|
40
|
-
1. Capture device state
|
|
189
|
+
1. Capture device state (State A)
|
|
41
190
|
2. Send to backend for AI decision
|
|
42
191
|
3. Execute returned action locally
|
|
43
|
-
4.
|
|
192
|
+
4. POLL until state changes (State B ≠ State A) or timeout
|
|
193
|
+
5. Send State B to backend in next iteration
|
|
194
|
+
6. Repeat until complete
|
|
195
|
+
|
|
196
|
+
This ensures the backend always sees the UPDATED state after each action,
|
|
197
|
+
preventing the agent from making decisions based on stale state.
|
|
44
198
|
|
|
45
199
|
Args:
|
|
46
200
|
task: Natural language task description
|
|
201
|
+
max_steps: Maximum number of steps to execute (default: 15)
|
|
47
202
|
progress_callback: Optional callback for progress updates
|
|
48
203
|
|
|
49
204
|
Returns:
|
|
@@ -117,6 +272,7 @@ async def execute_v3(
|
|
|
117
272
|
log_progress(f"🚀 Starting task: {task}")
|
|
118
273
|
log_progress(f"📱 Device: {state.device_serial}")
|
|
119
274
|
log_progress(f"🧠 Model: {config['model']}")
|
|
275
|
+
log_progress(f"🔢 Max steps: {max_steps}")
|
|
120
276
|
|
|
121
277
|
# Initialize execution
|
|
122
278
|
start_time = time.time()
|
|
@@ -139,7 +295,6 @@ async def execute_v3(
|
|
|
139
295
|
if describe_tools and DEFAULT and MahoragaAdbTools:
|
|
140
296
|
try:
|
|
141
297
|
# Create a mahoraga AdbTools instance for tool execution
|
|
142
|
-
# This instance has all the tool methods like swipe, start_app, etc.
|
|
143
298
|
mahoraga_tools = MahoragaAdbTools(
|
|
144
299
|
serial=state.device_serial,
|
|
145
300
|
use_tcp=True,
|
|
@@ -153,15 +308,26 @@ async def execute_v3(
|
|
|
153
308
|
allowed_tool_names = DEFAULT.allowed_tools if hasattr(DEFAULT, 'allowed_tools') else []
|
|
154
309
|
filtered_tools = {name: func for name, func in tool_list.items() if name in allowed_tool_names}
|
|
155
310
|
|
|
156
|
-
# Add each tool function to executor globals
|
|
311
|
+
# Add each tool function to executor globals with print wrapper
|
|
157
312
|
for tool_name, tool_function in filtered_tools.items():
|
|
158
313
|
# Convert async functions to sync if needed
|
|
159
314
|
if asyncio.iscoroutinefunction(tool_function):
|
|
160
315
|
if async_to_sync:
|
|
161
316
|
tool_function = async_to_sync(tool_function)
|
|
162
317
|
|
|
163
|
-
#
|
|
164
|
-
|
|
318
|
+
# Wrap tool function to print its return value
|
|
319
|
+
def make_printing_wrapper(func):
|
|
320
|
+
"""Wrap a tool function to print its return value."""
|
|
321
|
+
def wrapper(*args, **kwargs):
|
|
322
|
+
result = func(*args, **kwargs)
|
|
323
|
+
# Print the result so stdout captures it
|
|
324
|
+
if result is not None:
|
|
325
|
+
print(result)
|
|
326
|
+
return result
|
|
327
|
+
return wrapper
|
|
328
|
+
|
|
329
|
+
# Add wrapped function to globals so code can call it directly
|
|
330
|
+
executor_globals[tool_name] = make_printing_wrapper(tool_function)
|
|
165
331
|
|
|
166
332
|
log_progress(f"🔧 Loaded {len(filtered_tools)} tool functions: {list(filtered_tools.keys())}")
|
|
167
333
|
except Exception as e:
|
|
@@ -175,11 +341,11 @@ async def execute_v3(
|
|
|
175
341
|
# ============================================================
|
|
176
342
|
# STEP-BY-STEP EXECUTION LOOP
|
|
177
343
|
# ============================================================
|
|
178
|
-
while step_number <
|
|
344
|
+
while step_number < max_steps: # Use user-provided max_steps
|
|
179
345
|
step_number += 1
|
|
180
|
-
log_progress(f"🧠 Step {step_number}:
|
|
346
|
+
log_progress(f"🧠 Step {step_number}/{max_steps}: Analyzing...")
|
|
181
347
|
|
|
182
|
-
# 1. Capture device state
|
|
348
|
+
# 1. Capture device state (State A)
|
|
183
349
|
try:
|
|
184
350
|
ui_state_dict, screenshot_bytes = get_device_state(state.device_serial)
|
|
185
351
|
|
|
@@ -187,15 +353,14 @@ async def execute_v3(
|
|
|
187
353
|
if not config["vision"]:
|
|
188
354
|
screenshot_bytes = None
|
|
189
355
|
|
|
190
|
-
#
|
|
191
|
-
|
|
192
|
-
log_progress(f"📱
|
|
193
|
-
log_progress(f"📷 Screenshot: {'Present' if screenshot_bytes else 'None'}")
|
|
356
|
+
# Log current state
|
|
357
|
+
current_package = ui_state_dict.get("phone_state", {}).get("package", "unknown")
|
|
358
|
+
log_progress(f"📱 Current app: {current_package}")
|
|
194
359
|
|
|
195
360
|
except Exception as e:
|
|
196
361
|
log_progress(f"⚠️ Warning: Failed to capture device state: {e}")
|
|
197
362
|
ui_state_dict = {
|
|
198
|
-
"a11y_tree": "
|
|
363
|
+
"a11y_tree": [{"index": 0, "text": "Error capturing UI", "children": []}],
|
|
199
364
|
"phone_state": {"package": "unknown"}
|
|
200
365
|
}
|
|
201
366
|
screenshot_bytes = None
|
|
@@ -240,16 +405,6 @@ async def execute_v3(
|
|
|
240
405
|
code = action.get("code")
|
|
241
406
|
reasoning = action.get("reasoning")
|
|
242
407
|
|
|
243
|
-
# DEBUG: Log full backend response
|
|
244
|
-
log_progress(f"\n📋 DEBUG - Backend Response:")
|
|
245
|
-
log_progress(f" - Action type: {action_type}")
|
|
246
|
-
log_progress(f" - Completed: {step_result.get('completed', False)}")
|
|
247
|
-
log_progress(f" - Success: {step_result.get('success', None)}")
|
|
248
|
-
log_progress(f" - Code present: {bool(code)}")
|
|
249
|
-
if code:
|
|
250
|
-
log_progress(f" - Code: {code[:100]}..." if len(code) > 100 else f" - Code: {code}")
|
|
251
|
-
log_progress(f" - Assistant response: {step_result.get('assistant_response', '')[:200]}...\n")
|
|
252
|
-
|
|
253
408
|
# Log reasoning
|
|
254
409
|
if reasoning:
|
|
255
410
|
log_progress(f"🤔 Reasoning: {reasoning}")
|
|
@@ -296,17 +451,82 @@ async def execute_v3(
|
|
|
296
451
|
if code and action_type == "execute_code":
|
|
297
452
|
log_progress(f"⚡ Executing action...")
|
|
298
453
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
exec(code, executor_globals, executor_locals)
|
|
454
|
+
# Store old UI state for comparison
|
|
455
|
+
old_ui_state = ui_state_dict.copy()
|
|
302
456
|
|
|
303
|
-
|
|
304
|
-
|
|
457
|
+
try:
|
|
458
|
+
import io
|
|
459
|
+
import contextlib
|
|
460
|
+
|
|
461
|
+
# Capture stdout and stderr to get tool function outputs
|
|
462
|
+
stdout = io.StringIO()
|
|
463
|
+
stderr = io.StringIO()
|
|
464
|
+
|
|
465
|
+
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
466
|
+
# Execute code in sandbox
|
|
467
|
+
exec(code, executor_globals, executor_locals)
|
|
468
|
+
|
|
469
|
+
# Get captured output
|
|
470
|
+
execution_output = stdout.getvalue()
|
|
471
|
+
error_output = stderr.getvalue()
|
|
472
|
+
|
|
473
|
+
# ============================================================
|
|
474
|
+
# CRITICAL: Wait for state change (polling-based event detection)
|
|
475
|
+
# ============================================================
|
|
476
|
+
log_progress(f"⏳ Waiting for UI state to update...")
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
# Poll until state changes or timeout
|
|
480
|
+
new_ui_state_dict, _, state_changed = wait_for_action_effect(
|
|
481
|
+
get_device_state,
|
|
482
|
+
state.device_serial,
|
|
483
|
+
old_ui_state,
|
|
484
|
+
code,
|
|
485
|
+
min_wait=0.3,
|
|
486
|
+
poll_interval=0.5
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Log what happened
|
|
490
|
+
if state_changed:
|
|
491
|
+
old_pkg = old_ui_state.get("phone_state", {}).get("package", "")
|
|
492
|
+
new_pkg = new_ui_state_dict.get("phone_state", {}).get("package", "")
|
|
493
|
+
|
|
494
|
+
if old_pkg != new_pkg:
|
|
495
|
+
log_progress(f"✅ State changed: App switched ({old_pkg} → {new_pkg})")
|
|
496
|
+
else:
|
|
497
|
+
log_progress(f"✅ State changed: UI updated")
|
|
498
|
+
else:
|
|
499
|
+
log_progress(f"⚠️ WARNING: State did NOT change after action (timeout)")
|
|
500
|
+
log_progress(f" This might mean the action had no effect or took too long")
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
log_progress(f"⚠️ Error during state change detection: {e}")
|
|
504
|
+
state_changed = False
|
|
505
|
+
# Fallback: Just wait a bit
|
|
506
|
+
time.sleep(1.5)
|
|
507
|
+
|
|
508
|
+
# Build feedback message
|
|
509
|
+
feedback_parts = []
|
|
510
|
+
|
|
511
|
+
if execution_output:
|
|
512
|
+
feedback_parts.append(f"Action output: {execution_output.strip()}")
|
|
513
|
+
|
|
514
|
+
if state_changed:
|
|
515
|
+
feedback_parts.append("UI state updated successfully")
|
|
516
|
+
else:
|
|
517
|
+
feedback_parts.append("WARNING: UI state did not change (action may have failed)")
|
|
518
|
+
|
|
519
|
+
if error_output:
|
|
520
|
+
feedback_parts.append(f"Warnings: {error_output.strip()}")
|
|
521
|
+
|
|
522
|
+
feedback = " | ".join(feedback_parts) if feedback_parts else "Action executed"
|
|
523
|
+
|
|
524
|
+
log_progress(f"✅ {feedback[:200]}")
|
|
305
525
|
|
|
306
526
|
# Add execution result to chat history
|
|
307
527
|
chat_history.append({
|
|
308
528
|
"role": "user",
|
|
309
|
-
"content": f"Execution Result:\n```\n{
|
|
529
|
+
"content": f"Execution Result:\n```\n{feedback}\n```"
|
|
310
530
|
})
|
|
311
531
|
|
|
312
532
|
except Exception as e:
|
|
@@ -316,7 +536,7 @@ async def execute_v3(
|
|
|
316
536
|
# Add error to chat history
|
|
317
537
|
chat_history.append({
|
|
318
538
|
"role": "user",
|
|
319
|
-
"content": f"Execution
|
|
539
|
+
"content": f"Execution Error:\n```\n{error_msg}\n```"
|
|
320
540
|
})
|
|
321
541
|
|
|
322
542
|
else:
|
|
@@ -328,33 +548,78 @@ async def execute_v3(
|
|
|
328
548
|
})
|
|
329
549
|
|
|
330
550
|
# Max steps reached
|
|
331
|
-
log_progress(f"⚠️ Reached maximum steps ({
|
|
551
|
+
log_progress(f"⚠️ Reached maximum steps ({max_steps})")
|
|
332
552
|
log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
|
|
333
553
|
|
|
554
|
+
duration = time.time() - start_time
|
|
555
|
+
|
|
556
|
+
# Finalize session on backend to create execution record
|
|
557
|
+
await backend.finalize_session(
|
|
558
|
+
api_key=quash_api_key,
|
|
559
|
+
session_id=session_id,
|
|
560
|
+
task=task,
|
|
561
|
+
device_serial=state.device_serial,
|
|
562
|
+
status="max_steps",
|
|
563
|
+
final_message=f"Reached maximum step limit of {max_steps}",
|
|
564
|
+
error=None,
|
|
565
|
+
duration_seconds=duration,
|
|
566
|
+
config=config
|
|
567
|
+
)
|
|
568
|
+
|
|
334
569
|
return {
|
|
335
570
|
"status": "failed",
|
|
336
571
|
"steps_taken": step_number,
|
|
337
|
-
"final_message": f"Reached maximum step limit of {
|
|
572
|
+
"final_message": f"Reached maximum step limit of {max_steps}",
|
|
338
573
|
"message": "❌ Failed: Maximum steps reached",
|
|
339
574
|
"tokens": total_tokens,
|
|
340
575
|
"cost": total_cost,
|
|
341
|
-
"duration_seconds":
|
|
576
|
+
"duration_seconds": duration
|
|
342
577
|
}
|
|
343
578
|
|
|
344
579
|
except KeyboardInterrupt:
|
|
345
|
-
log_progress("
|
|
580
|
+
log_progress("ℹ️ Task interrupted by user")
|
|
581
|
+
duration = time.time() - start_time
|
|
582
|
+
|
|
583
|
+
# Finalize session on backend
|
|
584
|
+
await backend.finalize_session(
|
|
585
|
+
api_key=quash_api_key,
|
|
586
|
+
session_id=session_id,
|
|
587
|
+
task=task,
|
|
588
|
+
device_serial=state.device_serial,
|
|
589
|
+
status="interrupted",
|
|
590
|
+
final_message="Task interrupted by user",
|
|
591
|
+
error=None,
|
|
592
|
+
duration_seconds=duration,
|
|
593
|
+
config=config
|
|
594
|
+
)
|
|
595
|
+
|
|
346
596
|
return {
|
|
347
597
|
"status": "interrupted",
|
|
348
|
-
"message": "
|
|
598
|
+
"message": "ℹ️ Task execution interrupted",
|
|
349
599
|
"steps_taken": step_number,
|
|
350
600
|
"tokens": total_tokens,
|
|
351
601
|
"cost": total_cost,
|
|
352
|
-
"duration_seconds":
|
|
602
|
+
"duration_seconds": duration
|
|
353
603
|
}
|
|
354
604
|
|
|
355
605
|
except Exception as e:
|
|
356
606
|
error_msg = str(e)
|
|
357
607
|
log_progress(f"💥 Error: {error_msg}")
|
|
608
|
+
duration = time.time() - start_time
|
|
609
|
+
|
|
610
|
+
# Finalize session on backend
|
|
611
|
+
await backend.finalize_session(
|
|
612
|
+
api_key=quash_api_key,
|
|
613
|
+
session_id=session_id,
|
|
614
|
+
task=task,
|
|
615
|
+
device_serial=state.device_serial,
|
|
616
|
+
status="error",
|
|
617
|
+
final_message=None,
|
|
618
|
+
error=error_msg,
|
|
619
|
+
duration_seconds=duration,
|
|
620
|
+
config=config
|
|
621
|
+
)
|
|
622
|
+
|
|
358
623
|
return {
|
|
359
624
|
"status": "error",
|
|
360
625
|
"message": f"💥 Execution error: {error_msg}",
|
|
@@ -362,7 +627,7 @@ async def execute_v3(
|
|
|
362
627
|
"steps_taken": step_number,
|
|
363
628
|
"tokens": total_tokens,
|
|
364
629
|
"cost": total_cost,
|
|
365
|
-
"duration_seconds":
|
|
630
|
+
"duration_seconds": duration
|
|
366
631
|
}
|
|
367
632
|
|
|
368
633
|
finally:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quash-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
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
|
|
@@ -57,7 +57,7 @@ All dependencies (including ADB tools and device connectivity) are automatically
|
|
|
57
57
|
|
|
58
58
|
### 1. Get Your API Key
|
|
59
59
|
|
|
60
|
-
1. Visit [quashbugs.com](
|
|
60
|
+
1. Visit [quashbugs.com/mcp](http://13.220.180.140.nip.io/) (or your deployment URL)
|
|
61
61
|
2. Sign in with Google
|
|
62
62
|
3. Go to Dashboard → API Keys
|
|
63
63
|
4. Create a new API key
|
|
@@ -92,6 +92,22 @@ Add to your MCP host's config file:
|
|
|
92
92
|
- No PATH configuration needed
|
|
93
93
|
- Uses whichever Python has quash-mcp installed
|
|
94
94
|
|
|
95
|
+
#### CLI Configuration (If Supported by Host)
|
|
96
|
+
|
|
97
|
+
Some MCP hosts might provide a command-line interface to add servers.
|
|
98
|
+
|
|
99
|
+
**Examples:**
|
|
100
|
+
|
|
101
|
+
- **Claude Code:**
|
|
102
|
+
```bash
|
|
103
|
+
claude mcp add quash quash-mcp
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- **Gemini CLI:**
|
|
107
|
+
```bash
|
|
108
|
+
gemini mcp add quash quash-mcp
|
|
109
|
+
```
|
|
110
|
+
|
|
95
111
|
#### Alternative: Direct Command (if in PATH)
|
|
96
112
|
|
|
97
113
|
If `quash-mcp` is in your PATH:
|
|
@@ -220,7 +236,7 @@ User: "Show me my usage statistics"
|
|
|
220
236
|
|
|
221
237
|
- **Python 3.11+** - Required for the MCP server
|
|
222
238
|
- **Android Device** - Emulator or physical device with USB debugging enabled
|
|
223
|
-
- **Quash API Key** - Get from [quashbugs.com](
|
|
239
|
+
- **Quash API Key** - Get from [quashbugs.com/mcp](http://13.220.180.140.nip.io/)
|
|
224
240
|
|
|
225
241
|
Dependencies automatically installed:
|
|
226
242
|
- Android Debug Bridge (ADB) - via `adbutils`
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
quash_mcp/__init__.py,sha256=LImiWCRgjAbb5DZXBq2DktUEAbftvnO61Vil4Ayun9A,39
|
|
2
2
|
quash_mcp/__main__.py,sha256=WCg5OlnXhr6i0XJHAUGpbhliMy3qE2SJkFzVD4wO-lw,239
|
|
3
|
-
quash_mcp/backend_client.py,sha256=
|
|
3
|
+
quash_mcp/backend_client.py,sha256=ZqzYDO1sjD0LaPUaAY6y9dSWQRU4OBCdj-oqqmIdw6I,12793
|
|
4
4
|
quash_mcp/server.py,sha256=scUGnplxjsvyYLK2q6hrjl-5Chkdnat9pODDtLzsQFY,15519
|
|
5
5
|
quash_mcp/state.py,sha256=Tnt795GnZcas-h62Y6KYyIZVopeoWPM0TbRwOeVFYj4,4394
|
|
6
6
|
quash_mcp/device/__init__.py,sha256=6e8CtHolt-vJKPxZUU_Vsd6-QGqos9VrFykaLTT90rk,772
|
|
7
7
|
quash_mcp/device/adb_tools.py,sha256=Q0HIZHLBswaD94Qa7AA2VtjY31EZ68l0YClJDmMjOBU,3778
|
|
8
8
|
quash_mcp/device/portal.py,sha256=sDLJOruUwwNNxIDriiXB4vT0BZYILidgzVgdhHCEkDY,5241
|
|
9
|
-
quash_mcp/device/state_capture.py,sha256=
|
|
9
|
+
quash_mcp/device/state_capture.py,sha256=2aq7A-CwKRSbXZAob-JdoK6FfGfLJRB77tMYyo-O2Vo,3349
|
|
10
10
|
quash_mcp/tools/__init__.py,sha256=r4fMAjHDjHUbimRwYW7VYUDkQHs12UVsG_IBmWpeX9s,249
|
|
11
11
|
quash_mcp/tools/build.py,sha256=M6tGXWrQNkdtCYYrK14gUaoufQvyoor_hNN0lBPSVHY,30321
|
|
12
12
|
quash_mcp/tools/build_old.py,sha256=6M9gaqZ_dX4B7UFTxSMD8T1BX0zEwQUL7RJ8ItNfB54,6016
|
|
@@ -14,10 +14,10 @@ quash_mcp/tools/configure.py,sha256=cv4RTolu6qae-XzyACSJUDrALfd0gYC-XE5s66_zfNk,
|
|
|
14
14
|
quash_mcp/tools/connect.py,sha256=Kc7RGRUgtd2sR_bv6U4CB4kWSaLfsDc5kBo9u4FEjzs,4799
|
|
15
15
|
quash_mcp/tools/execute.py,sha256=kR3VzIl31Lek-js4Hgxs-S_ls4YwKnbqkt79KFbvFuM,909
|
|
16
16
|
quash_mcp/tools/execute_v2_backup.py,sha256=waWnaD0dEVcOJgRBbqZo3HnxME1s6YUOn8aRbm4R3X4,6081
|
|
17
|
-
quash_mcp/tools/execute_v3.py,sha256=
|
|
17
|
+
quash_mcp/tools/execute_v3.py,sha256=8WHiEcVWUcAekh7MGaqpXKYGAzdilQ1HaNankMoAxHI,23506
|
|
18
18
|
quash_mcp/tools/runsuite.py,sha256=gohLk9FpN8v7F0a69fspqOqUexTcslpYf3qU-iIZZ3s,7220
|
|
19
19
|
quash_mcp/tools/usage.py,sha256=g76A6FO36fThoyRFG7q92QmS3Kh1pIKOrhYOzUdIubA,1155
|
|
20
|
-
quash_mcp-0.2.
|
|
21
|
-
quash_mcp-0.2.
|
|
22
|
-
quash_mcp-0.2.
|
|
23
|
-
quash_mcp-0.2.
|
|
20
|
+
quash_mcp-0.2.10.dist-info/METADATA,sha256=fngQn032lI01_QIypgixbZFhe3bWWqA_1QqC-5qRS0w,8424
|
|
21
|
+
quash_mcp-0.2.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
22
|
+
quash_mcp-0.2.10.dist-info/entry_points.txt,sha256=9sbDxrx0ApGDVRS-IE3mQgSao3DwKnnV_k-_ipFn9QI,52
|
|
23
|
+
quash_mcp-0.2.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|