minitap-mcp 0.5.3__py3-none-any.whl → 0.7.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.
- minitap/mcp/core/agents/compare_screenshots/agent.py +12 -2
- minitap/mcp/core/cloud_apk.py +109 -0
- minitap/mcp/core/config.py +9 -4
- minitap/mcp/core/decorators.py +19 -1
- minitap/mcp/core/device.py +11 -4
- minitap/mcp/core/sdk_agent.py +8 -0
- minitap/mcp/core/storage.py +274 -0
- minitap/mcp/main.py +219 -41
- minitap/mcp/server/cloud_mobile.py +492 -0
- minitap/mcp/server/middleware.py +11 -13
- minitap/mcp/server/poller.py +6 -6
- minitap/mcp/server/remote_proxy.py +96 -0
- minitap/mcp/tools/execute_mobile_command.py +47 -2
- minitap/mcp/tools/read_swift_logs.py +297 -0
- minitap/mcp/tools/take_screenshot.py +53 -0
- minitap/mcp/tools/upload_screenshot.py +80 -0
- {minitap_mcp-0.5.3.dist-info → minitap_mcp-0.7.0.dist-info}/METADATA +59 -5
- minitap_mcp-0.7.0.dist-info/RECORD +34 -0
- {minitap_mcp-0.5.3.dist-info → minitap_mcp-0.7.0.dist-info}/WHEEL +2 -2
- minitap/mcp/tools/analyze_screen.py +0 -58
- minitap/mcp/tools/compare_screenshot_with_figma.py +0 -132
- minitap/mcp/tools/save_figma_assets.py +0 -276
- minitap_mcp-0.5.3.dist-info/RECORD +0 -30
- {minitap_mcp-0.5.3.dist-info → minitap_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
minitap/mcp/main.py
CHANGED
|
@@ -4,6 +4,9 @@ import argparse
|
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
6
|
import threading
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
7
10
|
|
|
8
11
|
# Fix Windows console encoding for Unicode characters (emojis in logs)
|
|
9
12
|
if sys.platform == "win32":
|
|
@@ -22,17 +25,20 @@ if sys.platform == "win32":
|
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
from fastmcp import FastMCP # noqa: E402
|
|
25
|
-
from minitap.mobile_use.config import settings as sdk_settings
|
|
26
28
|
|
|
27
29
|
from minitap.mcp.core.config import settings # noqa: E402
|
|
28
|
-
from minitap.mcp.core.device import
|
|
29
|
-
|
|
30
|
+
from minitap.mcp.core.device import (
|
|
31
|
+
DeviceInfo, # noqa: E402
|
|
32
|
+
list_available_devices,
|
|
33
|
+
)
|
|
30
34
|
from minitap.mcp.core.logging_config import (
|
|
31
35
|
configure_logging, # noqa: E402
|
|
32
36
|
get_logger,
|
|
33
37
|
)
|
|
34
|
-
from minitap.mcp.server.
|
|
38
|
+
from minitap.mcp.server.cloud_mobile import CloudMobileService
|
|
39
|
+
from minitap.mcp.server.middleware import LocalDeviceHealthMiddleware
|
|
35
40
|
from minitap.mcp.server.poller import device_health_poller
|
|
41
|
+
from minitap.mobile_use.config import settings as sdk_settings
|
|
36
42
|
|
|
37
43
|
configure_logging(log_level=os.getenv("LOG_LEVEL", "INFO"))
|
|
38
44
|
|
|
@@ -41,7 +47,20 @@ def main() -> None:
|
|
|
41
47
|
"""Main entry point for the MCP server."""
|
|
42
48
|
|
|
43
49
|
parser = argparse.ArgumentParser(description="Mobile Use MCP Server")
|
|
44
|
-
parser.add_argument(
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--api-key",
|
|
52
|
+
type=str,
|
|
53
|
+
required=False,
|
|
54
|
+
default=None,
|
|
55
|
+
help="Minitap API key for authentication",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--cloud-mobile-name",
|
|
59
|
+
type=str,
|
|
60
|
+
required=False,
|
|
61
|
+
default=None,
|
|
62
|
+
help="Name of the cloud mobile device to connect to (enables cloud mode)",
|
|
63
|
+
)
|
|
45
64
|
parser.add_argument("--llm-profile", type=str, required=False, default=None)
|
|
46
65
|
parser.add_argument(
|
|
47
66
|
"--server",
|
|
@@ -63,6 +82,11 @@ def main() -> None:
|
|
|
63
82
|
settings.__init__()
|
|
64
83
|
sdk_settings.__init__()
|
|
65
84
|
|
|
85
|
+
if args.cloud_mobile_name:
|
|
86
|
+
os.environ["CLOUD_MOBILE_NAME"] = args.cloud_mobile_name
|
|
87
|
+
settings.__init__()
|
|
88
|
+
sdk_settings.__init__()
|
|
89
|
+
|
|
66
90
|
if args.llm_profile:
|
|
67
91
|
os.environ["MINITAP_LLM_PROFILE_NAME"] = args.llm_profile
|
|
68
92
|
settings.__init__()
|
|
@@ -79,18 +103,194 @@ def main() -> None:
|
|
|
79
103
|
# Run MCP server with optional host/port for remote access
|
|
80
104
|
if args.server:
|
|
81
105
|
logger.info(f"Starting MCP server on {settings.MCP_SERVER_HOST}:{settings.MCP_SERVER_PORT}")
|
|
82
|
-
|
|
106
|
+
run_mcp_server(
|
|
83
107
|
transport="http",
|
|
84
108
|
host=settings.MCP_SERVER_HOST,
|
|
85
109
|
port=settings.MCP_SERVER_PORT,
|
|
86
110
|
)
|
|
87
111
|
else:
|
|
88
112
|
logger.info("Starting MCP server in local mode")
|
|
89
|
-
|
|
113
|
+
run_mcp_server()
|
|
90
114
|
|
|
91
115
|
|
|
92
116
|
logger = get_logger(__name__)
|
|
93
117
|
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class MCPLifespanContext:
|
|
121
|
+
"""Context for MCP server lifespan.
|
|
122
|
+
|
|
123
|
+
Stores references to services that need cleanup on shutdown.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
cloud_mobile_service: CloudMobileService | None = None
|
|
127
|
+
local_poller_stop_event: threading.Event | None = None
|
|
128
|
+
local_poller_thread: threading.Thread | None = None
|
|
129
|
+
remote_mcp_proxy: FastMCP | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@asynccontextmanager
|
|
133
|
+
async def mcp_lifespan(server: FastMCP) -> AsyncIterator[MCPLifespanContext]:
|
|
134
|
+
"""Lifespan context manager for MCP server.
|
|
135
|
+
|
|
136
|
+
Handles startup/shutdown for both cloud and local modes.
|
|
137
|
+
|
|
138
|
+
Cloud mode (CLOUD_MOBILE_NAME set):
|
|
139
|
+
- Connects to cloud mobile on startup
|
|
140
|
+
- Maintains keep-alive polling
|
|
141
|
+
- Disconnects on shutdown (critical for billing!)
|
|
142
|
+
|
|
143
|
+
Local mode (CLOUD_MOBILE_NAME not set):
|
|
144
|
+
- Starts device health poller
|
|
145
|
+
- Monitors local device connection
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
server: The FastMCP server instance.
|
|
149
|
+
|
|
150
|
+
Yields:
|
|
151
|
+
MCPLifespanContext with references to running services.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
RuntimeError: If cloud mobile connection fails
|
|
155
|
+
(crashes MCP to prevent false "connected" state).
|
|
156
|
+
"""
|
|
157
|
+
from minitap.mcp.core.sdk_agent import get_mobile_use_agent # noqa: E402
|
|
158
|
+
|
|
159
|
+
context = MCPLifespanContext()
|
|
160
|
+
api_key = settings.MINITAP_API_KEY.get_secret_value() if settings.MINITAP_API_KEY else None
|
|
161
|
+
|
|
162
|
+
# Check if running in cloud mode
|
|
163
|
+
if settings.CLOUD_MOBILE_NAME:
|
|
164
|
+
# ==================== CLOUD MODE ====================
|
|
165
|
+
logger.info(f"Starting MCP in CLOUD mode with mobile: {settings.CLOUD_MOBILE_NAME}")
|
|
166
|
+
|
|
167
|
+
if not api_key:
|
|
168
|
+
logger.error("MINITAP_API_KEY is required")
|
|
169
|
+
raise RuntimeError(
|
|
170
|
+
"MINITAP_API_KEY is required when CLOUD_MOBILE_NAME is set. "
|
|
171
|
+
"Please set your API key via --api-key or MINITAP_API_KEY environment variable."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Create and connect cloud mobile service
|
|
175
|
+
cloud_service = CloudMobileService(
|
|
176
|
+
cloud_mobile_name=settings.CLOUD_MOBILE_NAME,
|
|
177
|
+
api_key=api_key,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
await cloud_service.connect()
|
|
182
|
+
except Exception as e:
|
|
183
|
+
# CRITICAL: If cloud mobile not found, crash the MCP!
|
|
184
|
+
# This prevents the IDE from showing the server as "connected"
|
|
185
|
+
# when it actually can't do anything useful.
|
|
186
|
+
logger.error(f"Failed to connect to cloud mobile: {e}")
|
|
187
|
+
raise RuntimeError(
|
|
188
|
+
f"Cloud mobile connection failed. The MCP server cannot start.\n{e}"
|
|
189
|
+
) from e
|
|
190
|
+
|
|
191
|
+
context.cloud_mobile_service = cloud_service
|
|
192
|
+
logger.info("Cloud mobile connected, MCP server ready")
|
|
193
|
+
|
|
194
|
+
else:
|
|
195
|
+
# ==================== LOCAL MODE ====================
|
|
196
|
+
logger.info("Starting MCP in LOCAL mode (no CLOUD_MOBILE_NAME set)")
|
|
197
|
+
|
|
198
|
+
agent = get_mobile_use_agent()
|
|
199
|
+
server.add_middleware(LocalDeviceHealthMiddleware(agent))
|
|
200
|
+
|
|
201
|
+
# Start device health poller in background
|
|
202
|
+
stop_event = threading.Event()
|
|
203
|
+
poller_thread = threading.Thread(
|
|
204
|
+
target=device_health_poller,
|
|
205
|
+
args=(stop_event, agent),
|
|
206
|
+
daemon=True,
|
|
207
|
+
)
|
|
208
|
+
poller_thread.start()
|
|
209
|
+
|
|
210
|
+
context.local_poller_stop_event = stop_event
|
|
211
|
+
context.local_poller_thread = poller_thread
|
|
212
|
+
logger.info("Device health poller started")
|
|
213
|
+
|
|
214
|
+
# ==================== REMOTE MCP PROXY ====================
|
|
215
|
+
# Mount remote MCP proxy if configured (works in both cloud and local modes)
|
|
216
|
+
if settings.MINITAP_API_MCP_BASE_URL and api_key:
|
|
217
|
+
from minitap.mcp.server.remote_proxy import (
|
|
218
|
+
check_remote_mcp_availability,
|
|
219
|
+
create_remote_mcp_proxy,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
logger.info(f"Attempting to connect to remote MCP at: {settings.MINITAP_API_MCP_BASE_URL}")
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
is_available = await check_remote_mcp_availability(
|
|
226
|
+
mcp_url=settings.MINITAP_API_MCP_BASE_URL,
|
|
227
|
+
api_key=api_key,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if is_available:
|
|
231
|
+
remote_proxy = create_remote_mcp_proxy(
|
|
232
|
+
mcp_url=settings.MINITAP_API_MCP_BASE_URL,
|
|
233
|
+
api_key=api_key,
|
|
234
|
+
prefix="",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
server.mount(remote_proxy)
|
|
238
|
+
logger.info("Remote MCP proxy mounted successfully without prefix")
|
|
239
|
+
|
|
240
|
+
context.remote_mcp_proxy = remote_proxy
|
|
241
|
+
else:
|
|
242
|
+
logger.warning(
|
|
243
|
+
"Remote MCP server is not available. "
|
|
244
|
+
"Local tools will remain functional, but remote tools (Figma, Jira) "
|
|
245
|
+
"will not be accessible."
|
|
246
|
+
)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
# Log warning but don't crash - local tools should remain functional
|
|
249
|
+
logger.warning(
|
|
250
|
+
f"Failed to create remote MCP proxy: {e}. "
|
|
251
|
+
"Local tools will remain functional, but remote tools will not be accessible."
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
yield context
|
|
256
|
+
finally:
|
|
257
|
+
# ==================== SHUTDOWN ====================
|
|
258
|
+
logger.info("MCP server shutting down, cleaning up resources...")
|
|
259
|
+
|
|
260
|
+
if context.cloud_mobile_service:
|
|
261
|
+
# CRITICAL: Stop cloud mobile connection to stop billing!
|
|
262
|
+
logger.info("Disconnecting cloud mobile (stopping billing)...")
|
|
263
|
+
try:
|
|
264
|
+
await context.cloud_mobile_service.disconnect()
|
|
265
|
+
logger.info("Cloud mobile disconnected successfully")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.error(f"Error disconnecting cloud mobile: {e}")
|
|
268
|
+
|
|
269
|
+
if context.local_poller_stop_event and context.local_poller_thread:
|
|
270
|
+
# Stop local device health poller
|
|
271
|
+
logger.info("Stopping device health poller...")
|
|
272
|
+
context.local_poller_stop_event.set()
|
|
273
|
+
context.local_poller_thread.join(timeout=10.0)
|
|
274
|
+
|
|
275
|
+
if context.local_poller_thread.is_alive():
|
|
276
|
+
logger.warning("Device health poller thread did not stop gracefully")
|
|
277
|
+
else:
|
|
278
|
+
logger.info("Device health poller stopped successfully")
|
|
279
|
+
|
|
280
|
+
# Clean up remote MCP proxy connection
|
|
281
|
+
# basically not super important for now, but will be in the
|
|
282
|
+
# future when cloud dependencies must be cleaned up
|
|
283
|
+
if context.remote_mcp_proxy:
|
|
284
|
+
try:
|
|
285
|
+
logger.info("Cleaning up remote MCP proxy connection")
|
|
286
|
+
context.remote_mcp_proxy = None
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.warning(f"Error cleaning up remote MCP proxy: {e}")
|
|
289
|
+
|
|
290
|
+
logger.info("MCP server shutdown complete")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# Create MCP server with lifespan handler
|
|
94
294
|
mcp = FastMCP(
|
|
95
295
|
name="mobile-use-mcp",
|
|
96
296
|
instructions="""
|
|
@@ -98,11 +298,14 @@ mcp = FastMCP(
|
|
|
98
298
|
mobile devices (iOS or Android).
|
|
99
299
|
Call get_available_devices() to list them.
|
|
100
300
|
""",
|
|
301
|
+
lifespan=mcp_lifespan,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
from minitap.mcp.tools import ( # noqa: E402
|
|
305
|
+
execute_mobile_command, # noqa: E402, F401
|
|
306
|
+
read_swift_logs, # noqa: E402, F401
|
|
307
|
+
upload_screenshot, # noqa: E402, F401
|
|
101
308
|
)
|
|
102
|
-
from minitap.mcp.tools import analyze_screen # noqa: E402, F401
|
|
103
|
-
from minitap.mcp.tools import compare_screenshot_with_figma # noqa: E402, F401
|
|
104
|
-
from minitap.mcp.tools import execute_mobile_command # noqa: E402, F401
|
|
105
|
-
from minitap.mcp.tools import save_figma_assets # noqa: E402, F401
|
|
106
309
|
|
|
107
310
|
|
|
108
311
|
@mcp.resource("data://devices")
|
|
@@ -111,40 +314,15 @@ def get_available_devices() -> list[DeviceInfo]:
|
|
|
111
314
|
return list_available_devices()
|
|
112
315
|
|
|
113
316
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
agent = get_mobile_use_agent()
|
|
118
|
-
mcp.add_middleware(MaestroCheckerMiddleware(agent))
|
|
119
|
-
|
|
120
|
-
# Start device health poller in background
|
|
121
|
-
logger.info("Device health poller started")
|
|
122
|
-
stop_event = threading.Event()
|
|
123
|
-
poller_thread = threading.Thread(
|
|
124
|
-
target=device_health_poller,
|
|
125
|
-
args=(
|
|
126
|
-
stop_event,
|
|
127
|
-
agent,
|
|
128
|
-
),
|
|
129
|
-
daemon=True,
|
|
130
|
-
)
|
|
131
|
-
poller_thread.start()
|
|
317
|
+
def run_mcp_server(**mcp_run_kwargs):
|
|
318
|
+
"""Run the MCP server with proper exception handling.
|
|
132
319
|
|
|
320
|
+
This wraps mcp.run() with exception handling for clean shutdown.
|
|
321
|
+
"""
|
|
133
322
|
try:
|
|
134
323
|
mcp.run(**mcp_run_kwargs)
|
|
135
324
|
except KeyboardInterrupt:
|
|
136
325
|
logger.info("Keyboard interrupt received, shutting down...")
|
|
137
326
|
except Exception as e:
|
|
138
327
|
logger.error(f"Error running MCP server: {e}")
|
|
139
|
-
|
|
140
|
-
# Stop device health poller
|
|
141
|
-
logger.info("Stopping device health poller...")
|
|
142
|
-
stop_event.set()
|
|
143
|
-
|
|
144
|
-
# Give the poller thread a reasonable time to stop gracefully
|
|
145
|
-
poller_thread.join(timeout=10.0)
|
|
146
|
-
|
|
147
|
-
if poller_thread.is_alive():
|
|
148
|
-
logger.warning("Device health poller thread did not stop gracefully")
|
|
149
|
-
else:
|
|
150
|
-
logger.info("Device health poller stopped successfully")
|
|
328
|
+
raise
|