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.
@@ -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", "http://localhost:8000")
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
- logger.error(f"Failed to validate API key: {e}")
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)}"
@@ -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:
@@ -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.
@@ -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
- """Setup Quash Portal on the device."""
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 use_portal_apk, enable_portal_accessibility
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
- # Install APK
62
- with use_portal_apk(None, debug=False) as apk_path:
63
- device.install(apk_path, uninstall=True, flags=["-g"], silent=True)
64
-
65
- # Enable accessibility service
66
- enable_portal_accessibility(device)
67
-
68
- return True, "Portal installed and enabled successfully"
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
 
@@ -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 a single, powerful ADB tools instance from Mahoraga
272
- mahoraga_tools = None
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
- mahoraga_tools = MahoragaAdbTools(
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 MahoragaAdbTools: {e}")
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
- try:
293
- # Get all tool functions from the single mahoraga_tools instance
294
- tool_list = describe_tools(mahoraga_tools, exclude_tools=None)
295
-
296
- # Filter by allowed tools from DEFAULT persona
297
- allowed_tool_names = DEFAULT.allowed_tools if hasattr(DEFAULT, 'allowed_tools') else []
298
- filtered_tools = {name: func for name, func in tool_list.items() if name in allowed_tool_names}
299
-
300
- # Add each tool function to executor globals with print wrapper
301
- for tool_name, tool_function in filtered_tools.items():
302
- # Convert async functions to sync if needed
303
- if asyncio.iscoroutinefunction(tool_function):
304
- if async_to_sync:
305
- tool_function = async_to_sync(tool_function)
306
-
307
- # Wrap tool function to print its return value
308
- def make_printing_wrapper(func):
309
- """Wrap a tool function to print its return value."""
310
- def wrapper(*args, **kwargs):
311
- result = func(*args, **kwargs)
312
- # Print the result so stdout captures it
313
- if result is not None:
314
- print(result)
315
- return result
316
- return wrapper
317
-
318
- # Add wrapped function to globals so code can call it directly
319
- executor_globals[tool_name] = make_printing_wrapper(tool_function)
320
-
321
- log_progress(f"🔧 Loaded {len(filtered_tools)} tool functions: {list(filtered_tools.keys())}")
322
- except Exception as e:
323
- log_progress(f"⚠️ Warning: Could not load tool functions: {e}")
324
- import traceback
325
- log_progress(f"Traceback: {traceback.format_exc()}")
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 mahoraga_tools and "a11y_tree" in ui_state_dict and isinstance(ui_state_dict["a11y_tree"], list):
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
- mahoraga_tools.update_state(a11y_tree_obj)
355
+ adb_tools.update_state(a11y_tree_obj)
351
356
  except Exception as e:
352
- log_progress(f"⚠️ Warning: Failed to update mahoraga_tools state: {e}")
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
- # CRITICAL FIX: Check if complete() was actually called
446
- if mahoraga_tools and mahoraga_tools.finished:
447
- log_progress("✅ Agent has signaled task completion via complete()")
448
- session.last_action_completed = True
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
- # CRITICAL FIX: In reasoning mode with planning, DON'T exit on first complete() call
519
- # The backend controls when all tasks are done via the "complete" action type
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
- if success:
549
- log_progress(f"✅ Task completed successfully!")
550
- else:
551
- log_progress(f" Task marked as failed: {final_message}")
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" if success else "failed",
537
+ "status": "success",
562
538
  "steps_taken": len(session.steps),
563
- "final_message": final_message,
564
- "message": f"✅ Success: {final_message}" if success else f"❌ Failed: {final_message}",
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 mahoraga_tools:
625
- mahoraga_tools.teardown_tcp_forward()
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.0
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=_smBbhyJxN1dj89cJNVvqRZVX92oc1bw2SI1vzX2Rek,9979
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=SsYnzGjG3XsfbiAHiC7PpgLdC149kRH-YkoXQZvxvWc,5439
9
- quash_mcp/device/portal.py,sha256=sDLJOruUwwNNxIDriiXB4vT0BZYILidgzVgdhHCEkDY,5241
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=Kc7RGRUgtd2sR_bv6U4CB4kWSaLfsDc5kBo9u4FEjzs,4799
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=KMS7Zru2GiDcp1IqDvtGPhKlzbXEilCRp8hkvZShI2Q,24404
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.0.dist-info/METADATA,sha256=wLT-0D39eXubj1b8LiX57vBUae0ITz_1x1yCozxFUwM,8423
22
- quash_mcp-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- quash_mcp-0.3.0.dist-info/entry_points.txt,sha256=9sbDxrx0ApGDVRS-IE3mQgSao3DwKnnV_k-_ipFn9QI,52
24
- quash_mcp-0.3.0.dist-info/RECORD,,
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,,