quash-mcp 0.2.0__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/__init__.py +1 -0
- quash_mcp/__main__.py +10 -0
- quash_mcp/backend_client.py +203 -0
- quash_mcp/server.py +399 -0
- quash_mcp/state.py +137 -0
- quash_mcp/tools/__init__.py +9 -0
- quash_mcp/tools/build.py +739 -0
- quash_mcp/tools/build_old.py +185 -0
- quash_mcp/tools/configure.py +140 -0
- quash_mcp/tools/connect.py +153 -0
- quash_mcp/tools/execute.py +177 -0
- quash_mcp/tools/runsuite.py +209 -0
- quash_mcp/tools/usage.py +31 -0
- quash_mcp-0.2.0.dist-info/METADATA +271 -0
- quash_mcp-0.2.0.dist-info/RECORD +17 -0
- quash_mcp-0.2.0.dist-info/WHEEL +4 -0
- quash_mcp-0.2.0.dist-info/entry_points.txt +2 -0
quash_mcp/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Quash MCP Server - Source Package"""
|
quash_mcp/__main__.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend API Client for Quash MCP.
|
|
3
|
+
Handles all communication with the Quash backend API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import httpx
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BackendClient:
|
|
15
|
+
"""Client for communicating with Quash backend API."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
# Get backend URL from environment variable, default to localhost for development
|
|
19
|
+
self.base_url = os.getenv("MAHORAGA_BACKEND_URL", "http://localhost:8000")
|
|
20
|
+
self.timeout = 300.0 # 5 minutes for long-running LLM calls
|
|
21
|
+
logger.info(f"🔧 Backend client initialized: URL={self.base_url}")
|
|
22
|
+
|
|
23
|
+
async def validate_api_key(self, api_key: str) -> Dict[str, Any]:
|
|
24
|
+
"""
|
|
25
|
+
Validate quash_api_key and check user credits.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
api_key: The mahoraga API key to validate
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dict with validation result:
|
|
32
|
+
{
|
|
33
|
+
"valid": bool,
|
|
34
|
+
"user": {"email": str, "name": str, "credits": float},
|
|
35
|
+
"openrouter_api_key": str,
|
|
36
|
+
"error": str (if invalid)
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
41
|
+
response = await client.post(
|
|
42
|
+
f"{self.base_url}/api/validate",
|
|
43
|
+
json={"api_key": api_key}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if response.status_code == 200:
|
|
47
|
+
return response.json()
|
|
48
|
+
else:
|
|
49
|
+
return {
|
|
50
|
+
"valid": False,
|
|
51
|
+
"error": f"API error: {response.status_code}"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"Failed to validate API key: {e}")
|
|
56
|
+
return {
|
|
57
|
+
"valid": False,
|
|
58
|
+
"error": f"Connection error: {str(e)}"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async def execute_task(
|
|
62
|
+
self,
|
|
63
|
+
api_key: str,
|
|
64
|
+
task: str,
|
|
65
|
+
device_serial: str,
|
|
66
|
+
config: Dict[str, Any]
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Execute task with Quash agent on backend (V2 - AI on backend).
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
api_key: Quash API key
|
|
73
|
+
task: Task description
|
|
74
|
+
device_serial: Device serial number
|
|
75
|
+
config: Execution configuration (model, temp, vision, reasoning, etc.)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dict with execution result:
|
|
79
|
+
{
|
|
80
|
+
"status": "success"|"failed"|"error"|"interrupted",
|
|
81
|
+
"message": str,
|
|
82
|
+
"steps_taken": int,
|
|
83
|
+
"final_message": str,
|
|
84
|
+
"tokens": {"prompt": int, "completion": int, "total": int},
|
|
85
|
+
"cost": float,
|
|
86
|
+
"duration_seconds": float,
|
|
87
|
+
"error": str (if error)
|
|
88
|
+
}
|
|
89
|
+
"""
|
|
90
|
+
logger.info(f"🚀 Executing task on backend: {task[:50]}...")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
94
|
+
response = await client.post(
|
|
95
|
+
f"{self.base_url}/api/agent/execute",
|
|
96
|
+
json={
|
|
97
|
+
"api_key": api_key,
|
|
98
|
+
"task": task,
|
|
99
|
+
"device_serial": device_serial,
|
|
100
|
+
"config": config
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if response.status_code == 200:
|
|
105
|
+
result = response.json()
|
|
106
|
+
logger.info(f"✅ Backend execution completed: {result['status']}")
|
|
107
|
+
return result
|
|
108
|
+
else:
|
|
109
|
+
error_msg = f"Backend error: HTTP {response.status_code}"
|
|
110
|
+
logger.error(error_msg)
|
|
111
|
+
return {
|
|
112
|
+
"status": "error",
|
|
113
|
+
"message": error_msg,
|
|
114
|
+
"error": error_msg
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
error_msg = f"Failed to execute on backend: {str(e)}"
|
|
119
|
+
logger.error(error_msg)
|
|
120
|
+
return {
|
|
121
|
+
"status": "error",
|
|
122
|
+
"message": error_msg,
|
|
123
|
+
"error": str(e)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async def log_execution(
|
|
127
|
+
self,
|
|
128
|
+
api_key: str,
|
|
129
|
+
execution_id: str,
|
|
130
|
+
task: str,
|
|
131
|
+
device_serial: str,
|
|
132
|
+
status: str,
|
|
133
|
+
tokens: Optional[Dict[str, int]] = None,
|
|
134
|
+
cost: Optional[float] = None,
|
|
135
|
+
error: Optional[str] = None,
|
|
136
|
+
config: Optional[Dict[str, Any]] = None,
|
|
137
|
+
duration_seconds: Optional[float] = None
|
|
138
|
+
) -> Dict[str, Any]:
|
|
139
|
+
"""
|
|
140
|
+
Log execution completion and usage to backend.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
api_key: Quash API key
|
|
144
|
+
execution_id: Unique execution identifier
|
|
145
|
+
task: Task description
|
|
146
|
+
device_serial: Device serial number
|
|
147
|
+
status: "completed", "failed", or "interrupted"
|
|
148
|
+
tokens: Token usage dict
|
|
149
|
+
cost: Execution cost in USD
|
|
150
|
+
error: Error message if failed
|
|
151
|
+
config: Execution configuration (model, temp, vision, etc.)
|
|
152
|
+
duration_seconds: Time taken to complete
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dict with logging result:
|
|
156
|
+
{
|
|
157
|
+
"logged": bool,
|
|
158
|
+
"credits_deducted": float,
|
|
159
|
+
"new_balance": float,
|
|
160
|
+
"error": str (if failed)
|
|
161
|
+
}
|
|
162
|
+
"""
|
|
163
|
+
logger.info(f"📊 Logging execution - Cost: ${cost}, Status: {status}")
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
167
|
+
response = await client.post(
|
|
168
|
+
f"{self.base_url}/api/execution/complete",
|
|
169
|
+
json={
|
|
170
|
+
"api_key": api_key,
|
|
171
|
+
"execution_id": execution_id,
|
|
172
|
+
"task": task,
|
|
173
|
+
"device_serial": device_serial,
|
|
174
|
+
"status": status,
|
|
175
|
+
"tokens": tokens,
|
|
176
|
+
"cost": cost,
|
|
177
|
+
"error": error,
|
|
178
|
+
"config": config,
|
|
179
|
+
"duration_seconds": duration_seconds
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if response.status_code == 200:
|
|
184
|
+
return response.json()
|
|
185
|
+
else:
|
|
186
|
+
logger.warning(f"Failed to log execution: {response.status_code}")
|
|
187
|
+
return {"logged": False, "error": f"HTTP {response.status_code}"}
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.error(f"Failed to log execution: {e}")
|
|
191
|
+
return {"logged": False, "error": str(e)}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Singleton instance
|
|
195
|
+
_backend_client = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_backend_client() -> BackendClient:
|
|
199
|
+
"""Get the global backend client instance."""
|
|
200
|
+
global _backend_client
|
|
201
|
+
if _backend_client is None:
|
|
202
|
+
_backend_client = BackendClient()
|
|
203
|
+
return _backend_client
|
quash_mcp/server.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Quash MCP Server
|
|
4
|
+
A Model Context Protocol server for mobile automation testing with Quash.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from dotenv import load_dotenv
|
|
12
|
+
from mcp.server import Server
|
|
13
|
+
from mcp.server.stdio import stdio_server
|
|
14
|
+
from mcp.types import Tool, TextContent
|
|
15
|
+
|
|
16
|
+
# Load .env file from project root
|
|
17
|
+
project_root = Path(__file__).parent.parent
|
|
18
|
+
env_file = project_root / ".env"
|
|
19
|
+
load_dotenv(env_file)
|
|
20
|
+
|
|
21
|
+
from .tools.build import build
|
|
22
|
+
from .tools.connect import connect
|
|
23
|
+
from .tools.configure import configure
|
|
24
|
+
from .tools.execute import execute
|
|
25
|
+
from .tools.runsuite import runsuite
|
|
26
|
+
from .tools.usage import usage
|
|
27
|
+
from .state import get_state
|
|
28
|
+
|
|
29
|
+
# Setup logging
|
|
30
|
+
logging.basicConfig(level=logging.INFO)
|
|
31
|
+
logger = logging.getLogger("quash-mcp")
|
|
32
|
+
|
|
33
|
+
# Create MCP server instance
|
|
34
|
+
app = Server("quash-mcp")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.list_tools()
|
|
38
|
+
async def list_tools() -> list[Tool]:
|
|
39
|
+
"""List all available Quash tools with dynamic state information."""
|
|
40
|
+
|
|
41
|
+
# Get current state
|
|
42
|
+
state = get_state()
|
|
43
|
+
|
|
44
|
+
# Build dynamic descriptions
|
|
45
|
+
|
|
46
|
+
# CONNECT tool description
|
|
47
|
+
connect_desc = ("Connect to an Android device or emulator. "
|
|
48
|
+
"Auto-detects single device or allows selection from multiple devices. "
|
|
49
|
+
"Verifies connectivity and checks/installs Quash Portal accessibility service.")
|
|
50
|
+
|
|
51
|
+
if state.is_device_connected():
|
|
52
|
+
connect_desc += f"\n\n📱 CURRENT DEVICE:\n"
|
|
53
|
+
connect_desc += f" • Serial: {state.device_serial}\n"
|
|
54
|
+
if state.device_info:
|
|
55
|
+
connect_desc += f" • Model: {state.device_info.get('model', 'Unknown')}\n"
|
|
56
|
+
connect_desc += f" • Android: {state.device_info.get('android_version', 'Unknown')}\n"
|
|
57
|
+
connect_desc += f" • Portal: {'✓ Ready' if state.portal_ready else '✗ Not Ready'}"
|
|
58
|
+
else:
|
|
59
|
+
connect_desc += "\n\n📱 CURRENT DEVICE: Not connected"
|
|
60
|
+
|
|
61
|
+
# CONFIGURE tool description
|
|
62
|
+
configure_desc = ("Configure Quash agent execution parameters. "
|
|
63
|
+
"Set API key, model, temperature, max steps, and enable/disable vision, reasoning, and reflection features. "
|
|
64
|
+
"Only updates parameters that are provided.")
|
|
65
|
+
|
|
66
|
+
# Mask API key for display (show first 10 and last 6 chars)
|
|
67
|
+
api_key = state.config.get('api_key')
|
|
68
|
+
if api_key:
|
|
69
|
+
if len(api_key) < 20:
|
|
70
|
+
masked_key = api_key[:4] + "..." + api_key[-4:]
|
|
71
|
+
else:
|
|
72
|
+
masked_key = api_key[:10] + "..." + api_key[-6:]
|
|
73
|
+
api_key_display = masked_key
|
|
74
|
+
else:
|
|
75
|
+
api_key_display = "✗ Not Set"
|
|
76
|
+
|
|
77
|
+
configure_desc += f"\n\n⚙️ CURRENT CONFIGURATION:\n"
|
|
78
|
+
configure_desc += f" • API Key: {api_key_display}\n"
|
|
79
|
+
configure_desc += f" • Model: {state.config.get('model', 'anthropic/claude-sonnet-4')}\n"
|
|
80
|
+
configure_desc += f" • Temperature: {state.config.get('temperature', 0.2)}\n"
|
|
81
|
+
configure_desc += f" • Max Steps: {state.config.get('max_steps', 15)}\n"
|
|
82
|
+
configure_desc += f" • Vision: {'✓ Enabled' if state.config.get('vision') else '✗ Disabled'}\n"
|
|
83
|
+
configure_desc += f" • Reasoning: {'✓ Enabled' if state.config.get('reasoning') else '✗ Disabled'}\n"
|
|
84
|
+
configure_desc += f" • Reflection: {'✓ Enabled' if state.config.get('reflection') else '✗ Disabled'}\n"
|
|
85
|
+
configure_desc += f" • Debug: {'✓ Enabled' if state.config.get('debug') else '✗ Disabled'}"
|
|
86
|
+
|
|
87
|
+
# RUNSUITE tool description with latest execution
|
|
88
|
+
runsuite_desc = ("Execute a suite of tasks in sequence. "
|
|
89
|
+
"Runs multiple tasks with support for retries, failure handling, and wait times. "
|
|
90
|
+
"Provides detailed execution summary with pass rates and task-by-task results. "
|
|
91
|
+
"Requires device to be connected and configuration to be set.")
|
|
92
|
+
|
|
93
|
+
# Show latest suite execution if available
|
|
94
|
+
if state.latest_suite:
|
|
95
|
+
suite = state.latest_suite
|
|
96
|
+
runsuite_desc += f"\n\n📋 LATEST SUITE: {suite['suite_name']}\n"
|
|
97
|
+
runsuite_desc += f" • Status: {suite['status'].upper()}\n"
|
|
98
|
+
runsuite_desc += f" • Pass Rate: {suite['pass_rate']}%\n"
|
|
99
|
+
runsuite_desc += f" • Duration: {suite['duration_seconds']}s\n"
|
|
100
|
+
runsuite_desc += f" • Tasks: {suite['completed_tasks']}/{suite['total_tasks']} completed\n"
|
|
101
|
+
|
|
102
|
+
# Show individual task results
|
|
103
|
+
runsuite_desc += "\n Task Results:\n"
|
|
104
|
+
for task_result in suite.get('task_results', []):
|
|
105
|
+
task_num = task_result['task_number']
|
|
106
|
+
task_status = task_result['status']
|
|
107
|
+
task_prompt = task_result.get('prompt', 'No prompt')[:40]
|
|
108
|
+
|
|
109
|
+
# Status indicator
|
|
110
|
+
if task_status == 'completed':
|
|
111
|
+
indicator = "✅"
|
|
112
|
+
elif task_status == 'failed':
|
|
113
|
+
indicator = "❌"
|
|
114
|
+
else:
|
|
115
|
+
indicator = "⏭️"
|
|
116
|
+
|
|
117
|
+
runsuite_desc += f" {indicator} Task {task_num}: {task_prompt}... - {task_status.upper()}\n"
|
|
118
|
+
else:
|
|
119
|
+
runsuite_desc += "\n\n📋 LATEST SUITE: No suites executed yet"
|
|
120
|
+
|
|
121
|
+
# USAGE tool description
|
|
122
|
+
usage_desc = ("View usage statistics and costs for your Quash executions. "
|
|
123
|
+
"All usage tracking happens on the backend for security. "
|
|
124
|
+
"Directs you to the web dashboard for detailed statistics.")
|
|
125
|
+
|
|
126
|
+
return [
|
|
127
|
+
Tool(
|
|
128
|
+
name="build",
|
|
129
|
+
description="Setup and verify all dependencies required for Quash mobile automation. "
|
|
130
|
+
"Checks Python version, ADB installation, Quash package, and Portal APK. "
|
|
131
|
+
"Attempts to auto-install missing dependencies where possible.",
|
|
132
|
+
inputSchema={
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {},
|
|
135
|
+
"required": []
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
Tool(
|
|
139
|
+
name="connect",
|
|
140
|
+
description=connect_desc,
|
|
141
|
+
inputSchema={
|
|
142
|
+
"type": "object",
|
|
143
|
+
"properties": {
|
|
144
|
+
"device_serial": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"description": "Device serial number (optional - auto-detects if only one device)"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"required": []
|
|
150
|
+
}
|
|
151
|
+
),
|
|
152
|
+
Tool(
|
|
153
|
+
name="configure",
|
|
154
|
+
description=configure_desc,
|
|
155
|
+
inputSchema={
|
|
156
|
+
"type": "object",
|
|
157
|
+
"properties": {
|
|
158
|
+
"quash_api_key": {
|
|
159
|
+
"type": "string",
|
|
160
|
+
"description": "Quash API key for authentication and access"
|
|
161
|
+
},
|
|
162
|
+
"model": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"description": "LLM model name (e.g., 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet')"
|
|
165
|
+
},
|
|
166
|
+
"temperature": {
|
|
167
|
+
"type": "number",
|
|
168
|
+
"description": "Temperature for LLM sampling (0-2, default 0.2)"
|
|
169
|
+
},
|
|
170
|
+
"max_steps": {
|
|
171
|
+
"type": "integer",
|
|
172
|
+
"description": "Maximum number of execution steps (default 15)"
|
|
173
|
+
},
|
|
174
|
+
"vision": {
|
|
175
|
+
"type": "boolean",
|
|
176
|
+
"description": "Enable vision capabilities using screenshots (default false)"
|
|
177
|
+
},
|
|
178
|
+
"reasoning": {
|
|
179
|
+
"type": "boolean",
|
|
180
|
+
"description": "Enable planning with reasoning for complex tasks (default false)"
|
|
181
|
+
},
|
|
182
|
+
"reflection": {
|
|
183
|
+
"type": "boolean",
|
|
184
|
+
"description": "Enable reflection for self-improvement (default false)"
|
|
185
|
+
},
|
|
186
|
+
"debug": {
|
|
187
|
+
"type": "boolean",
|
|
188
|
+
"description": "Enable verbose debug logging (default false)"
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"required": []
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
Tool(
|
|
195
|
+
name="execute",
|
|
196
|
+
description="Execute a mobile automation task on the connected Android device. "
|
|
197
|
+
"Takes natural language instructions and performs the task using AI agents. "
|
|
198
|
+
"Provides live progress updates during execution. "
|
|
199
|
+
"Requires device to be connected and configuration to be set.",
|
|
200
|
+
inputSchema={
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"task": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"description": "Natural language description of the task to perform (e.g., 'Open Settings and navigate to WiFi')"
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"required": ["task"]
|
|
209
|
+
}
|
|
210
|
+
),
|
|
211
|
+
Tool(
|
|
212
|
+
name="runsuite",
|
|
213
|
+
description=runsuite_desc,
|
|
214
|
+
inputSchema={
|
|
215
|
+
"type": "object",
|
|
216
|
+
"properties": {
|
|
217
|
+
"suite_name": {
|
|
218
|
+
"type": "string",
|
|
219
|
+
"description": "Name of the test suite being executed"
|
|
220
|
+
},
|
|
221
|
+
"tasks": {
|
|
222
|
+
"type": "array",
|
|
223
|
+
"description": "Array of task definitions to execute in sequence",
|
|
224
|
+
"items": {
|
|
225
|
+
"type": "object",
|
|
226
|
+
"properties": {
|
|
227
|
+
"prompt": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"description": "Task instruction (required)"
|
|
230
|
+
},
|
|
231
|
+
"type": {
|
|
232
|
+
"type": "string",
|
|
233
|
+
"description": "Task type: 'setup', 'test', or 'teardown' (optional, default 'test')",
|
|
234
|
+
"enum": ["setup", "test", "teardown"]
|
|
235
|
+
},
|
|
236
|
+
"retries": {
|
|
237
|
+
"type": "integer",
|
|
238
|
+
"description": "Number of retry attempts on failure (optional, default 0)"
|
|
239
|
+
},
|
|
240
|
+
"continueOnFailure": {
|
|
241
|
+
"type": "boolean",
|
|
242
|
+
"description": "Continue suite execution if this task fails (optional, default false)"
|
|
243
|
+
},
|
|
244
|
+
"waitBefore": {
|
|
245
|
+
"type": "integer",
|
|
246
|
+
"description": "Seconds to wait before executing task (optional, default 0)"
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
"required": ["prompt"]
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"required": ["suite_name", "tasks"]
|
|
254
|
+
}
|
|
255
|
+
),
|
|
256
|
+
Tool(
|
|
257
|
+
name="usage",
|
|
258
|
+
description=usage_desc,
|
|
259
|
+
inputSchema={
|
|
260
|
+
"type": "object",
|
|
261
|
+
"properties": {
|
|
262
|
+
"api_key": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"description": "Specific API key to query (optional - shows all if not provided)"
|
|
265
|
+
},
|
|
266
|
+
"show_recent": {
|
|
267
|
+
"type": "integer",
|
|
268
|
+
"description": "Number of recent executions to show (default: 5)"
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
"required": []
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@app.call_tool()
|
|
278
|
+
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
|
|
279
|
+
"""Handle tool execution."""
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
if name == "build":
|
|
283
|
+
result = await build()
|
|
284
|
+
|
|
285
|
+
elif name == "connect":
|
|
286
|
+
device_serial = arguments.get("device_serial")
|
|
287
|
+
result = await connect(device_serial=device_serial)
|
|
288
|
+
|
|
289
|
+
elif name == "configure":
|
|
290
|
+
result = await configure(
|
|
291
|
+
quash_api_key=arguments.get("quash_api_key"),
|
|
292
|
+
model=arguments.get("model"),
|
|
293
|
+
temperature=arguments.get("temperature"),
|
|
294
|
+
max_steps=arguments.get("max_steps"),
|
|
295
|
+
vision=arguments.get("vision"),
|
|
296
|
+
reasoning=arguments.get("reasoning"),
|
|
297
|
+
reflection=arguments.get("reflection"),
|
|
298
|
+
debug=arguments.get("debug")
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
elif name == "execute":
|
|
302
|
+
task = arguments.get("task")
|
|
303
|
+
if not task:
|
|
304
|
+
return [TextContent(
|
|
305
|
+
type="text",
|
|
306
|
+
text="❌ Error: 'task' parameter is required"
|
|
307
|
+
)]
|
|
308
|
+
|
|
309
|
+
# Collect progress messages
|
|
310
|
+
progress_messages = []
|
|
311
|
+
|
|
312
|
+
def progress_callback(message: str):
|
|
313
|
+
progress_messages.append(message)
|
|
314
|
+
|
|
315
|
+
result = await execute(task=task, progress_callback=progress_callback)
|
|
316
|
+
|
|
317
|
+
# Combine progress messages with result
|
|
318
|
+
if progress_messages:
|
|
319
|
+
result["execution_log"] = "\n".join(progress_messages)
|
|
320
|
+
|
|
321
|
+
elif name == "runsuite":
|
|
322
|
+
suite_name = arguments.get("suite_name")
|
|
323
|
+
tasks = arguments.get("tasks")
|
|
324
|
+
|
|
325
|
+
if not suite_name:
|
|
326
|
+
return [TextContent(
|
|
327
|
+
type="text",
|
|
328
|
+
text="❌ Error: 'suite_name' parameter is required"
|
|
329
|
+
)]
|
|
330
|
+
|
|
331
|
+
if not tasks or not isinstance(tasks, list) or len(tasks) == 0:
|
|
332
|
+
return [TextContent(
|
|
333
|
+
type="text",
|
|
334
|
+
text="❌ Error: 'tasks' must be a non-empty array"
|
|
335
|
+
)]
|
|
336
|
+
|
|
337
|
+
# Collect progress messages
|
|
338
|
+
progress_messages = []
|
|
339
|
+
|
|
340
|
+
def progress_callback(message: str):
|
|
341
|
+
progress_messages.append(message)
|
|
342
|
+
|
|
343
|
+
result = await runsuite(
|
|
344
|
+
suite_name=suite_name,
|
|
345
|
+
tasks=tasks,
|
|
346
|
+
progress_callback=progress_callback
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Combine progress messages with result
|
|
350
|
+
if progress_messages:
|
|
351
|
+
result["execution_log"] = "\n".join(progress_messages)
|
|
352
|
+
|
|
353
|
+
elif name == "usage":
|
|
354
|
+
result = await usage(
|
|
355
|
+
api_key=arguments.get("api_key"),
|
|
356
|
+
show_recent=arguments.get("show_recent", 5)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
else:
|
|
360
|
+
return [TextContent(
|
|
361
|
+
type="text",
|
|
362
|
+
text=f"❌ Unknown tool: {name}"
|
|
363
|
+
)]
|
|
364
|
+
|
|
365
|
+
# Format result as text
|
|
366
|
+
import json
|
|
367
|
+
result_text = json.dumps(result, indent=2)
|
|
368
|
+
|
|
369
|
+
return [TextContent(
|
|
370
|
+
type="text",
|
|
371
|
+
text=result_text
|
|
372
|
+
)]
|
|
373
|
+
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error(f"Error executing tool {name}: {e}", exc_info=True)
|
|
376
|
+
return [TextContent(
|
|
377
|
+
type="text",
|
|
378
|
+
text=f"❌ Error executing {name}: {str(e)}"
|
|
379
|
+
)]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
async def async_main():
|
|
383
|
+
"""Run the MCP server (async)."""
|
|
384
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
385
|
+
logger.info("🚀 Quash MCP Server started")
|
|
386
|
+
await app.run(
|
|
387
|
+
read_stream,
|
|
388
|
+
write_stream,
|
|
389
|
+
app.create_initialization_options()
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def main():
|
|
394
|
+
"""Entry point for the quash-mcp command."""
|
|
395
|
+
asyncio.run(async_main())
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
if __name__ == "__main__":
|
|
399
|
+
main()
|