quash-mcp 0.3.0__py3-none-any.whl → 0.3.3__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.
- quash_mcp/backend_client.py +4 -2
- quash_mcp/device/adb_tools.py +128 -2
- quash_mcp/device/portal.py +17 -0
- quash_mcp/tools/connect.py +31 -10
- quash_mcp/tools/execute_v3.py +64 -85
- {quash_mcp-0.3.0.dist-info → quash_mcp-0.3.3.dist-info}/METADATA +1 -1
- {quash_mcp-0.3.0.dist-info → quash_mcp-0.3.3.dist-info}/RECORD +9 -9
- {quash_mcp-0.3.0.dist-info → quash_mcp-0.3.3.dist-info}/WHEEL +0 -0
- {quash_mcp-0.3.0.dist-info → quash_mcp-0.3.3.dist-info}/entry_points.txt +0 -0
quash_mcp/backend_client.py
CHANGED
|
@@ -20,7 +20,7 @@ class BackendClient:
|
|
|
20
20
|
|
|
21
21
|
def __init__(self):
|
|
22
22
|
# Get backend URL from environment variable, default to production backend
|
|
23
|
-
self.base_url = os.getenv("MAHORAGA_BACKEND_URL", "
|
|
23
|
+
self.base_url = os.getenv("MAHORAGA_BACKEND_URL", "https://mcpbe.quashbugs.com")
|
|
24
24
|
self.timeout = 300.0 # 5 minutes for long-running LLM calls
|
|
25
25
|
logger.info(f"🔧 Backend client initialized: URL={self.base_url}")
|
|
26
26
|
|
|
@@ -57,7 +57,9 @@ class BackendClient:
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
except Exception as e:
|
|
60
|
-
|
|
60
|
+
import traceback
|
|
61
|
+
error_details = traceback.format_exc()
|
|
62
|
+
logger.error(f"Failed to validate API key: {e}\n{error_details}")
|
|
61
63
|
return {
|
|
62
64
|
"valid": False,
|
|
63
65
|
"error": f"Connection error: {str(e)}"
|
quash_mcp/device/adb_tools.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
2
|
ADB Tools - Basic Android device communication wrapper.
|
|
3
3
|
Simplified version for device management without agent-specific functionality.
|
|
4
|
+
Includes lightweight tool functions for executing backend-generated code.
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
7
|
import logging
|
|
7
|
-
from typing import Optional
|
|
8
|
+
from typing import Optional, List, Dict, Any
|
|
8
9
|
from adbutils import adb
|
|
9
10
|
import requests
|
|
10
11
|
|
|
@@ -13,7 +14,7 @@ PORTAL_DEFAULT_TCP_PORT = 8080
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class AdbTools:
|
|
16
|
-
"""Basic ADB device communication wrapper."""
|
|
17
|
+
"""Basic ADB device communication wrapper with tool execution functions."""
|
|
17
18
|
|
|
18
19
|
def __init__(
|
|
19
20
|
self,
|
|
@@ -32,6 +33,7 @@ class AdbTools:
|
|
|
32
33
|
self.use_tcp = use_tcp
|
|
33
34
|
self.remote_tcp_port = remote_tcp_port
|
|
34
35
|
self.tcp_forwarded = False
|
|
36
|
+
self.clickable_elements_cache: List[Dict[str, Any]] = []
|
|
35
37
|
|
|
36
38
|
# Set up TCP forwarding if requested
|
|
37
39
|
if self.use_tcp:
|
|
@@ -144,6 +146,130 @@ class AdbTools:
|
|
|
144
146
|
logger.error(f"Failed to get screenshot: {e}", exc_info=True)
|
|
145
147
|
return b""
|
|
146
148
|
|
|
149
|
+
# === Tool Execution Functions ===
|
|
150
|
+
# These lightweight functions are used to execute backend-generated code
|
|
151
|
+
|
|
152
|
+
def update_state(self, a11y_tree: List[Dict[str, Any]]) -> None:
|
|
153
|
+
"""Update clickable elements cache from accessibility tree."""
|
|
154
|
+
try:
|
|
155
|
+
elements = a11y_tree
|
|
156
|
+
filtered_elements = []
|
|
157
|
+
for element in elements:
|
|
158
|
+
filtered_element = {k: v for k, v in element.items() if k != "type"}
|
|
159
|
+
if "children" in filtered_element:
|
|
160
|
+
filtered_element["children"] = [
|
|
161
|
+
{k: v for k, v in child.items() if k != "type"}
|
|
162
|
+
for child in filtered_element["children"]
|
|
163
|
+
]
|
|
164
|
+
filtered_elements.append(filtered_element)
|
|
165
|
+
self.clickable_elements_cache = filtered_elements
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"Failed to update state: {e}")
|
|
168
|
+
|
|
169
|
+
def tap_by_index(self, index: int) -> str:
|
|
170
|
+
"""Tap on element by index."""
|
|
171
|
+
try:
|
|
172
|
+
if not self.clickable_elements_cache:
|
|
173
|
+
return "Error: No UI elements cached"
|
|
174
|
+
|
|
175
|
+
def find_element_by_index(elements, target_idx):
|
|
176
|
+
for item in elements:
|
|
177
|
+
if item.get("index") == target_idx:
|
|
178
|
+
return item
|
|
179
|
+
children = item.get("children", [])
|
|
180
|
+
result = find_element_by_index(children, target_idx)
|
|
181
|
+
if result:
|
|
182
|
+
return result
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
element = find_element_by_index(self.clickable_elements_cache, index)
|
|
186
|
+
if not element:
|
|
187
|
+
return f"Error: No element found with index {index}"
|
|
188
|
+
|
|
189
|
+
bounds_str = element.get("bounds")
|
|
190
|
+
if not bounds_str:
|
|
191
|
+
return f"Error: Element at index {index} has no bounds"
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
left, top, right, bottom = map(int, bounds_str.split(","))
|
|
195
|
+
x = (left + right) // 2
|
|
196
|
+
y = (top + bottom) // 2
|
|
197
|
+
self.device.shell(f"input tap {x} {y}")
|
|
198
|
+
return f"Tapped on element at index {index}"
|
|
199
|
+
except Exception as e:
|
|
200
|
+
return f"Error parsing bounds: {e}"
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(f"Failed to tap by index {index}: {e}")
|
|
203
|
+
return f"Error: {e}"
|
|
204
|
+
|
|
205
|
+
def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 500) -> str:
|
|
206
|
+
"""Swipe from start to end coordinates."""
|
|
207
|
+
try:
|
|
208
|
+
self.device.shell(f"input swipe {start_x} {start_y} {end_x} {end_y} {duration}")
|
|
209
|
+
return "Swiped successfully"
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(f"Failed to swipe: {e}")
|
|
212
|
+
return f"Error: {e}"
|
|
213
|
+
|
|
214
|
+
def input_text(self, text: str) -> str:
|
|
215
|
+
"""Input text into the focused field."""
|
|
216
|
+
try:
|
|
217
|
+
escaped_text = text.replace('"', '\\"').replace('$', '\\$')
|
|
218
|
+
self.device.shell(f'input text "{escaped_text}"')
|
|
219
|
+
return f"Text entered: {text}"
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(f"Failed to input text: {e}")
|
|
222
|
+
return f"Error: {e}"
|
|
223
|
+
|
|
224
|
+
def press_key(self, key: str) -> str:
|
|
225
|
+
"""Press a key on the device."""
|
|
226
|
+
try:
|
|
227
|
+
key_map = {
|
|
228
|
+
"BACK": "4",
|
|
229
|
+
"HOME": "3",
|
|
230
|
+
"MENU": "82",
|
|
231
|
+
"SEARCH": "84",
|
|
232
|
+
"ENTER": "66",
|
|
233
|
+
"TAB": "61",
|
|
234
|
+
"SPACE": "62",
|
|
235
|
+
}
|
|
236
|
+
key_code = key_map.get(key.upper(), key)
|
|
237
|
+
self.device.shell(f"input keyevent {key_code}")
|
|
238
|
+
return f"Key pressed: {key}"
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Failed to press key: {e}")
|
|
241
|
+
return f"Error: {e}"
|
|
242
|
+
|
|
243
|
+
def start_app(self, package_name: str) -> str:
|
|
244
|
+
"""Start an app by package name."""
|
|
245
|
+
try:
|
|
246
|
+
self.device.shell(f"monkey -p {package_name} 1")
|
|
247
|
+
return f"App started: {package_name}"
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error(f"Failed to start app: {e}")
|
|
250
|
+
return f"Error: {e}"
|
|
251
|
+
|
|
252
|
+
def complete(self, success: bool = True, reason: str = "") -> str:
|
|
253
|
+
"""Signal task completion."""
|
|
254
|
+
status = "SUCCESS" if success else "FAILED"
|
|
255
|
+
return f"Task completed ({status}): {reason}"
|
|
256
|
+
|
|
257
|
+
def remember(self, text: str) -> str:
|
|
258
|
+
"""Store text in memory."""
|
|
259
|
+
return f"Remembered: {text}"
|
|
260
|
+
|
|
261
|
+
def list_packages(self, filter_str: str = "") -> str:
|
|
262
|
+
"""List installed packages."""
|
|
263
|
+
try:
|
|
264
|
+
result = self.device.shell("pm list packages")
|
|
265
|
+
packages = result.strip().split('\n')
|
|
266
|
+
if filter_str:
|
|
267
|
+
packages = [p for p in packages if filter_str.lower() in p.lower()]
|
|
268
|
+
return "\n".join(packages[:20])
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error(f"Failed to list packages: {e}")
|
|
271
|
+
return f"Error: {e}"
|
|
272
|
+
|
|
147
273
|
def __del__(self):
|
|
148
274
|
"""Cleanup when the object is destroyed."""
|
|
149
275
|
if hasattr(self, "tcp_forwarded") and self.tcp_forwarded:
|
quash_mcp/device/portal.py
CHANGED
|
@@ -124,6 +124,23 @@ def check_portal_accessibility(device: AdbDevice, debug: bool = False) -> bool:
|
|
|
124
124
|
return True
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
def is_portal_installed(device: AdbDevice, debug: bool = False) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Check if Quash Portal package is installed on the device.
|
|
130
|
+
Does NOT check if accessibility service is enabled.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
packages = device.list_packages()
|
|
134
|
+
is_installed = PORTAL_PACKAGE_NAME in packages
|
|
135
|
+
if debug:
|
|
136
|
+
print(f"Portal installed: {is_installed}")
|
|
137
|
+
return is_installed
|
|
138
|
+
except Exception as e:
|
|
139
|
+
if debug:
|
|
140
|
+
print(f"Error checking packages: {e}")
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
127
144
|
def ping_portal(device: AdbDevice, debug: bool = False):
|
|
128
145
|
"""
|
|
129
146
|
Ping the Quash Portal to check if it is installed and accessible.
|
quash_mcp/tools/connect.py
CHANGED
|
@@ -51,21 +51,42 @@ def check_portal_service(serial: str) -> bool:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def setup_portal(serial: str) -> tuple[bool, str]:
|
|
54
|
-
"""
|
|
54
|
+
"""
|
|
55
|
+
Setup Quash Portal on the device.
|
|
56
|
+
|
|
57
|
+
If Portal is already installed, only enables the accessibility service.
|
|
58
|
+
If Portal is not installed, attempts to install and enable it.
|
|
59
|
+
"""
|
|
55
60
|
try:
|
|
56
61
|
from adbutils import adb
|
|
57
|
-
from quash_mcp.device.portal import
|
|
62
|
+
from quash_mcp.device.portal import (
|
|
63
|
+
is_portal_installed,
|
|
64
|
+
enable_portal_accessibility,
|
|
65
|
+
use_portal_apk
|
|
66
|
+
)
|
|
58
67
|
|
|
59
68
|
device = adb.device(serial)
|
|
60
69
|
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
# Check if Portal is already installed
|
|
71
|
+
if is_portal_installed(device, debug=False):
|
|
72
|
+
# Portal already installed, just enable accessibility service
|
|
73
|
+
enable_portal_accessibility(device)
|
|
74
|
+
return True, "Portal accessibility service enabled"
|
|
75
|
+
else:
|
|
76
|
+
# Portal not installed, try to install it
|
|
77
|
+
try:
|
|
78
|
+
with use_portal_apk(None, debug=False) as apk_path:
|
|
79
|
+
device.install(apk_path, uninstall=True, flags=["-g"], silent=True)
|
|
80
|
+
|
|
81
|
+
# Enable accessibility service after installation
|
|
82
|
+
enable_portal_accessibility(device)
|
|
83
|
+
return True, "Portal installed and enabled successfully"
|
|
84
|
+
except Exception as install_error:
|
|
85
|
+
# Installation failed, report that Portal needs to be manually installed
|
|
86
|
+
return False, (
|
|
87
|
+
f"Portal not installed and auto-install failed: {str(install_error)}. "
|
|
88
|
+
"Please manually install the Portal APK on the device."
|
|
89
|
+
)
|
|
69
90
|
except Exception as e:
|
|
70
91
|
return False, f"Failed to setup portal: {str(e)}"
|
|
71
92
|
|
quash_mcp/tools/execute_v3.py
CHANGED
|
@@ -17,7 +17,9 @@ from ..state import get_state
|
|
|
17
17
|
from ..backend_client import get_backend_client
|
|
18
18
|
from ..device.state_capture import get_device_state
|
|
19
19
|
from ..device.adb_tools import AdbTools
|
|
20
|
+
|
|
20
21
|
import logging
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
21
23
|
|
|
22
24
|
# Import mahoraga components for tool functions
|
|
23
25
|
try:
|
|
@@ -268,16 +270,18 @@ async def execute_v3(
|
|
|
268
270
|
last_action_completed=None # Explicitly initialize the new field
|
|
269
271
|
)
|
|
270
272
|
|
|
271
|
-
# Initialize
|
|
272
|
-
|
|
273
|
+
# Initialize ADB tools for executing generated code
|
|
274
|
+
# Use the lightweight AdbTools from quash-mcp device module (no mahoraga dependency)
|
|
275
|
+
adb_tools = None
|
|
273
276
|
try:
|
|
274
|
-
|
|
277
|
+
adb_tools = AdbTools(
|
|
275
278
|
serial=state.device_serial,
|
|
276
279
|
use_tcp=True,
|
|
277
280
|
remote_tcp_port=8080
|
|
278
281
|
)
|
|
282
|
+
log_progress(f"✅ Initialized AdbTools for code execution")
|
|
279
283
|
except Exception as e:
|
|
280
|
-
log_progress(f"⚠️ CRITICAL: Failed to initialize
|
|
284
|
+
log_progress(f"⚠️ CRITICAL: Failed to initialize AdbTools: {e}")
|
|
281
285
|
return {
|
|
282
286
|
"status": "error",
|
|
283
287
|
"message": f"💥 Failed to initialize ADB tools: {e}",
|
|
@@ -289,40 +293,41 @@ async def execute_v3(
|
|
|
289
293
|
}
|
|
290
294
|
|
|
291
295
|
# Add tool functions to executor namespace
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
#
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
296
|
+
# Use the lightweight tool functions from adb_tools (no mahoraga dependency needed)
|
|
297
|
+
if adb_tools:
|
|
298
|
+
try:
|
|
299
|
+
tool_functions = {
|
|
300
|
+
'tap_by_index': adb_tools.tap_by_index,
|
|
301
|
+
'swipe': adb_tools.swipe,
|
|
302
|
+
'input_text': adb_tools.input_text,
|
|
303
|
+
'press_key': adb_tools.press_key,
|
|
304
|
+
'start_app': adb_tools.start_app,
|
|
305
|
+
'complete': adb_tools.complete,
|
|
306
|
+
'remember': adb_tools.remember,
|
|
307
|
+
'list_packages': adb_tools.list_packages,
|
|
308
|
+
'update_state': adb_tools.update_state,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Add each tool function to executor globals with print wrapper
|
|
312
|
+
for tool_name, tool_function in tool_functions.items():
|
|
313
|
+
def make_printing_wrapper(func):
|
|
314
|
+
"""Wrap a tool function to print its return value."""
|
|
315
|
+
def wrapper(*args, **kwargs):
|
|
316
|
+
result = func(*args, **kwargs)
|
|
317
|
+
# Print the result so stdout captures it
|
|
318
|
+
if result is not None:
|
|
319
|
+
print(result)
|
|
320
|
+
return result
|
|
321
|
+
return wrapper
|
|
322
|
+
|
|
323
|
+
# Add wrapped function to globals so code can call it directly
|
|
324
|
+
executor_globals[tool_name] = make_printing_wrapper(tool_function)
|
|
325
|
+
|
|
326
|
+
log_progress(f"🔧 Loaded {len(tool_functions)} tool functions: {list(tool_functions.keys())}")
|
|
327
|
+
except Exception as e:
|
|
328
|
+
log_progress(f"⚠️ Warning: Could not load tool functions: {e}")
|
|
329
|
+
import traceback
|
|
330
|
+
log_progress(f"Traceback: {traceback.format_exc()}")
|
|
326
331
|
|
|
327
332
|
executor_locals = {}
|
|
328
333
|
|
|
@@ -344,12 +349,12 @@ async def execute_v3(
|
|
|
344
349
|
session.ui_state = UIStateInfo(**ui_state_dict)
|
|
345
350
|
|
|
346
351
|
# Update local tools with new state
|
|
347
|
-
if
|
|
352
|
+
if adb_tools and "a11y_tree" in ui_state_dict and isinstance(ui_state_dict["a11y_tree"], list):
|
|
348
353
|
try:
|
|
349
354
|
a11y_tree_obj = ui_state_dict["a11y_tree"]
|
|
350
|
-
|
|
355
|
+
adb_tools.update_state(a11y_tree_obj)
|
|
351
356
|
except Exception as e:
|
|
352
|
-
log_progress(f"⚠️ Warning: Failed to update
|
|
357
|
+
log_progress(f"⚠️ Warning: Failed to update adb_tools state: {e}")
|
|
353
358
|
|
|
354
359
|
if not config["vision"]:
|
|
355
360
|
screenshot_bytes = None
|
|
@@ -442,12 +447,10 @@ async def execute_v3(
|
|
|
442
447
|
execution_output = stdout.getvalue()
|
|
443
448
|
error_output = stderr.getvalue()
|
|
444
449
|
|
|
445
|
-
#
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
else:
|
|
450
|
-
session.last_action_completed = False
|
|
450
|
+
# NOTE: Check if complete() was called in the executed code
|
|
451
|
+
# For now, we rely on the backend to tell us when tasks are complete
|
|
452
|
+
# via the action type, not through tool properties
|
|
453
|
+
session.last_action_completed = False
|
|
451
454
|
|
|
452
455
|
log_progress(f"⏳ Waiting for UI state to update...")
|
|
453
456
|
try:
|
|
@@ -515,40 +518,13 @@ async def execute_v3(
|
|
|
515
518
|
))
|
|
516
519
|
|
|
517
520
|
# 4. Check if overall task is complete
|
|
518
|
-
#
|
|
519
|
-
|
|
520
|
-
should_exit = False
|
|
521
|
-
|
|
522
|
-
if mahoraga_tools and mahoraga_tools.finished:
|
|
523
|
-
# Check if this is the FINAL completion from the backend
|
|
524
|
-
# In reasoning mode, the backend returns action.type="complete" when ALL tasks are done
|
|
525
|
-
action_type = action.get("type", "")
|
|
526
|
-
|
|
527
|
-
if action_type == "complete":
|
|
528
|
-
# Backend explicitly says we're done with ALL tasks
|
|
529
|
-
should_exit = True
|
|
530
|
-
success = mahoraga_tools.success
|
|
531
|
-
final_message = mahoraga_tools.reason
|
|
532
|
-
elif config["reasoning"] and session.current_plan:
|
|
533
|
-
# In reasoning mode with a plan, a single complete() call is just for one sub-task
|
|
534
|
-
# Continue the loop - the backend will advance to the next task
|
|
535
|
-
log_progress(f"✅ Sub-task completed. Moving to next task...")
|
|
536
|
-
should_exit = False
|
|
537
|
-
else:
|
|
538
|
-
# Non-reasoning mode: first complete() means done
|
|
539
|
-
should_exit = True
|
|
540
|
-
success = mahoraga_tools.success
|
|
541
|
-
final_message = mahoraga_tools.reason
|
|
542
|
-
|
|
543
|
-
if should_exit and mahoraga_tools and mahoraga_tools.finished:
|
|
544
|
-
success = mahoraga_tools.success
|
|
545
|
-
final_message = mahoraga_tools.reason
|
|
546
|
-
duration = time.time() - start_time
|
|
521
|
+
# The backend controls task completion via action.type == "complete"
|
|
522
|
+
action_type = action.get("type", "")
|
|
547
523
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
524
|
+
if action_type == "complete":
|
|
525
|
+
# Backend explicitly says we're done with ALL tasks
|
|
526
|
+
duration = time.time() - start_time
|
|
527
|
+
log_progress(f"✅ Task completed successfully!")
|
|
552
528
|
|
|
553
529
|
# Finalize session on backend
|
|
554
530
|
finalize_result = await backend.finalize_session(session=session)
|
|
@@ -558,10 +534,10 @@ async def execute_v3(
|
|
|
558
534
|
log_progress(f"💰 Usage: {total_tokens.get('total')} tokens, ${total_cost:.4f}")
|
|
559
535
|
|
|
560
536
|
return {
|
|
561
|
-
"status": "success"
|
|
537
|
+
"status": "success",
|
|
562
538
|
"steps_taken": len(session.steps),
|
|
563
|
-
"final_message":
|
|
564
|
-
"message": f"✅ Success:
|
|
539
|
+
"final_message": "Task completed successfully",
|
|
540
|
+
"message": f"✅ Success: Task completed",
|
|
565
541
|
"tokens": total_tokens,
|
|
566
542
|
"cost": total_cost,
|
|
567
543
|
"duration_seconds": duration
|
|
@@ -621,5 +597,8 @@ async def execute_v3(
|
|
|
621
597
|
|
|
622
598
|
finally:
|
|
623
599
|
# Cleanup TCP forwarding
|
|
624
|
-
if
|
|
625
|
-
|
|
600
|
+
if adb_tools:
|
|
601
|
+
try:
|
|
602
|
+
adb_tools.teardown_tcp_forward()
|
|
603
|
+
except Exception as e:
|
|
604
|
+
logger.warning(f"Failed to cleanup TCP forwarding: {e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quash-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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,24 +1,24 @@
|
|
|
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=yxlzw5KPqlNTObDLumCxacrxZXz7T28mEHW9oEvXlNw,10082
|
|
4
4
|
quash_mcp/models.py,sha256=zqi0-DCmgOaq4TiuJsb9QsQxMxcJ82B3NeRwbnrfJQc,1414
|
|
5
5
|
quash_mcp/server.py,sha256=scUGnplxjsvyYLK2q6hrjl-5Chkdnat9pODDtLzsQFY,15519
|
|
6
6
|
quash_mcp/state.py,sha256=Tnt795GnZcas-h62Y6KYyIZVopeoWPM0TbRwOeVFYj4,4394
|
|
7
7
|
quash_mcp/device/__init__.py,sha256=6e8CtHolt-vJKPxZUU_Vsd6-QGqos9VrFykaLTT90rk,772
|
|
8
|
-
quash_mcp/device/adb_tools.py,sha256=
|
|
9
|
-
quash_mcp/device/portal.py,sha256=
|
|
8
|
+
quash_mcp/device/adb_tools.py,sha256=N73iTnxoCRzLDegLbQfCrFj7GUisRdfuwfweH1h3sOo,10620
|
|
9
|
+
quash_mcp/device/portal.py,sha256=qt2iC26ocPQF0L3oKnlOvG7dvgumKhW5WvN5Da8gbcE,5774
|
|
10
10
|
quash_mcp/device/state_capture.py,sha256=NwuhjCBI576w9eexhdVOxfsOmABTW1A4SWRpcjadg-w,4016
|
|
11
11
|
quash_mcp/tools/__init__.py,sha256=r4fMAjHDjHUbimRwYW7VYUDkQHs12UVsG_IBmWpeX9s,249
|
|
12
12
|
quash_mcp/tools/build.py,sha256=M6tGXWrQNkdtCYYrK14gUaoufQvyoor_hNN0lBPSVHY,30321
|
|
13
13
|
quash_mcp/tools/build_old.py,sha256=6M9gaqZ_dX4B7UFTxSMD8T1BX0zEwQUL7RJ8ItNfB54,6016
|
|
14
14
|
quash_mcp/tools/configure.py,sha256=cv4RTolu6qae-XzyACSJUDrALfd0gYC-XE5s66_zfNk,4439
|
|
15
|
-
quash_mcp/tools/connect.py,sha256=
|
|
15
|
+
quash_mcp/tools/connect.py,sha256=Etu4qhHCmGj_pQJ2TiD-vpLj5PHzzJnQop25PTk6jQM,5783
|
|
16
16
|
quash_mcp/tools/execute.py,sha256=kR3VzIl31Lek-js4Hgxs-S_ls4YwKnbqkt79KFbvFuM,909
|
|
17
17
|
quash_mcp/tools/execute_v2_backup.py,sha256=waWnaD0dEVcOJgRBbqZo3HnxME1s6YUOn8aRbm4R3X4,6081
|
|
18
|
-
quash_mcp/tools/execute_v3.py,sha256=
|
|
18
|
+
quash_mcp/tools/execute_v3.py,sha256=g8qfGjXrZBc0n7Qm5xkqf_5zJ5h5JtgZ6Yb51kNLNTc,23062
|
|
19
19
|
quash_mcp/tools/runsuite.py,sha256=gohLk9FpN8v7F0a69fspqOqUexTcslpYf3qU-iIZZ3s,7220
|
|
20
20
|
quash_mcp/tools/usage.py,sha256=g76A6FO36fThoyRFG7q92QmS3Kh1pIKOrhYOzUdIubA,1155
|
|
21
|
-
quash_mcp-0.3.
|
|
22
|
-
quash_mcp-0.3.
|
|
23
|
-
quash_mcp-0.3.
|
|
24
|
-
quash_mcp-0.3.
|
|
21
|
+
quash_mcp-0.3.3.dist-info/METADATA,sha256=Zn5e20vNUQ4ai5P3C5jL5Y8CgvEK9EXlakMBjx22dVg,8423
|
|
22
|
+
quash_mcp-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
quash_mcp-0.3.3.dist-info/entry_points.txt,sha256=9sbDxrx0ApGDVRS-IE3mQgSao3DwKnnV_k-_ipFn9QI,52
|
|
24
|
+
quash_mcp-0.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|