quash-mcp 0.2.9__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.

@@ -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
@@ -1,14 +1,18 @@
1
1
  """
2
- Execute tool V3 - Step-by-step execution with local device access.
2
+ Execute tool V3 - Step-by-step execution with state-change verification.
3
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.
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
- from typing import Dict, Any, Callable, Optional
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 locally (UI + optional screenshot)
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. Repeat until complete
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
- # Add to globals so code can call it directly
164
- executor_globals[tool_name] = tool_function
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 < 15: # Max 15 steps
344
+ while step_number < max_steps: # Use user-provided max_steps
179
345
  step_number += 1
180
- log_progress(f"🧠 Step {step_number}: Thinking...")
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
- # DEBUG: Log UI state
191
- a11y_preview = ui_state_dict.get("a11y_tree", "")[:150]
192
- log_progress(f"📱 UI State captured - A11y tree preview: {a11y_preview}...")
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": "<hierarchy></hierarchy>",
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
- try:
300
- # Execute code in sandbox
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
- # Get execution result
304
- execution_output = executor_locals.get("_result", "Code executed successfully")
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{execution_output}\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 Result:\n```\n{error_msg}\n```"
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 ({step_number})")
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 {step_number}",
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": time.time() - start_time
576
+ "duration_seconds": duration
342
577
  }
343
578
 
344
579
  except KeyboardInterrupt:
345
- log_progress("⏹️ Task interrupted by user")
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": "⏹️ Task execution interrupted",
598
+ "message": "ℹ️ Task execution interrupted",
349
599
  "steps_taken": step_number,
350
600
  "tokens": total_tokens,
351
601
  "cost": total_cost,
352
- "duration_seconds": time.time() - start_time
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": time.time() - start_time
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.9
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](https://quashbugs.com) (or your deployment URL)
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](https://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,6 +1,6 @@
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=FvrchO5R4fByTlSh_LosFSKXOZSsPKYEsRZ3LNioDdk,9998
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
@@ -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=Ya3XEh2fOBgVmytIShLkiWuAbpQKE4CNGUSamwJD92I,14250
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.9.dist-info/METADATA,sha256=l22Tc-iKIlk17y6MzziulgCopPJ4kdkKq_P_Bw470rU,8129
21
- quash_mcp-0.2.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- quash_mcp-0.2.9.dist-info/entry_points.txt,sha256=9sbDxrx0ApGDVRS-IE3mQgSao3DwKnnV_k-_ipFn9QI,52
23
- quash_mcp-0.2.9.dist-info/RECORD,,
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,,