minitap-mcp 0.9.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.
Files changed (35) hide show
  1. minitap/mcp/__init__.py +0 -0
  2. minitap/mcp/core/agents/compare_screenshots/agent.py +75 -0
  3. minitap/mcp/core/agents/compare_screenshots/eval/prompts/prompt_1.md +62 -0
  4. minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/actual.png +0 -0
  5. minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/figma.png +0 -0
  6. minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/human_feedback.txt +18 -0
  7. minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/model_params.json +3 -0
  8. minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/output.md +46 -0
  9. minitap/mcp/core/agents/compare_screenshots/prompt.md +62 -0
  10. minitap/mcp/core/cloud_apk.py +117 -0
  11. minitap/mcp/core/config.py +111 -0
  12. minitap/mcp/core/decorators.py +107 -0
  13. minitap/mcp/core/device.py +249 -0
  14. minitap/mcp/core/llm.py +39 -0
  15. minitap/mcp/core/logging_config.py +59 -0
  16. minitap/mcp/core/models.py +59 -0
  17. minitap/mcp/core/sdk_agent.py +35 -0
  18. minitap/mcp/core/storage.py +407 -0
  19. minitap/mcp/core/task_runs.py +100 -0
  20. minitap/mcp/core/utils/figma.py +69 -0
  21. minitap/mcp/core/utils/images.py +55 -0
  22. minitap/mcp/main.py +328 -0
  23. minitap/mcp/server/cloud_mobile.py +492 -0
  24. minitap/mcp/server/middleware.py +21 -0
  25. minitap/mcp/server/poller.py +78 -0
  26. minitap/mcp/server/remote_proxy.py +96 -0
  27. minitap/mcp/tools/execute_mobile_command.py +182 -0
  28. minitap/mcp/tools/read_swift_logs.py +297 -0
  29. minitap/mcp/tools/screen_analyzer.md +17 -0
  30. minitap/mcp/tools/take_screenshot.py +53 -0
  31. minitap/mcp/tools/upload_screenshot.py +80 -0
  32. minitap_mcp-0.9.0.dist-info/METADATA +352 -0
  33. minitap_mcp-0.9.0.dist-info/RECORD +35 -0
  34. minitap_mcp-0.9.0.dist-info/WHEEL +4 -0
  35. minitap_mcp-0.9.0.dist-info/entry_points.txt +3 -0
minitap/mcp/main.py ADDED
@@ -0,0 +1,328 @@
1
+ """MCP server for mobile-use with screen analysis capabilities."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+ import threading
7
+ from collections.abc import AsyncIterator
8
+ from contextlib import asynccontextmanager
9
+ from dataclasses import dataclass
10
+
11
+ # Fix Windows console encoding for Unicode characters (emojis in logs)
12
+ if sys.platform == "win32":
13
+ if hasattr(sys.stdout, "reconfigure"):
14
+ sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
15
+ if hasattr(sys.stderr, "reconfigure"):
16
+ sys.stderr.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
17
+ os.environ["PYTHONIOENCODING"] = "utf-8"
18
+
19
+ try:
20
+ import colorama
21
+
22
+ colorama.init(strip=False, convert=True, wrap=True)
23
+ except ImportError:
24
+ pass
25
+
26
+
27
+ from fastmcp import FastMCP # noqa: E402
28
+
29
+ from minitap.mcp.core.config import settings # noqa: E402
30
+ from minitap.mcp.core.device import (
31
+ DeviceInfo, # noqa: E402
32
+ list_available_devices,
33
+ )
34
+ from minitap.mcp.core.logging_config import (
35
+ configure_logging, # noqa: E402
36
+ get_logger,
37
+ )
38
+ from minitap.mcp.server.cloud_mobile import CloudMobileService
39
+ from minitap.mcp.server.middleware import LocalDeviceHealthMiddleware
40
+ from minitap.mcp.server.poller import device_health_poller
41
+ from minitap.mobile_use.config import settings as sdk_settings
42
+
43
+ configure_logging(log_level=os.getenv("LOG_LEVEL", "INFO"))
44
+
45
+
46
+ def main() -> None:
47
+ """Main entry point for the MCP server."""
48
+
49
+ parser = argparse.ArgumentParser(description="Mobile Use MCP Server")
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
+ )
64
+ parser.add_argument("--llm-profile", type=str, required=False, default=None)
65
+ parser.add_argument(
66
+ "--server",
67
+ action="store_true",
68
+ help="Run as network server (uses MCP_SERVER_HOST and MCP_SERVER_PORT from env)",
69
+ )
70
+ parser.add_argument(
71
+ "--port",
72
+ type=int,
73
+ required=False,
74
+ default=None,
75
+ help="Port to run the server on (overrides MCP_SERVER_PORT env variable)",
76
+ )
77
+
78
+ args = parser.parse_args()
79
+
80
+ if args.api_key:
81
+ os.environ["MINITAP_API_KEY"] = args.api_key
82
+ settings.__init__()
83
+ sdk_settings.__init__()
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
+
90
+ if args.llm_profile:
91
+ os.environ["MINITAP_LLM_PROFILE_NAME"] = args.llm_profile
92
+ settings.__init__()
93
+ sdk_settings.__init__()
94
+
95
+ if args.port:
96
+ os.environ["MCP_SERVER_PORT"] = str(args.port)
97
+ settings.__init__()
98
+ sdk_settings.__init__()
99
+
100
+ if not settings.MINITAP_API_KEY:
101
+ raise ValueError("Minitap API key is required to run the MCP")
102
+
103
+ # Run MCP server with optional host/port for remote access
104
+ if args.server:
105
+ logger.info(f"Starting MCP server on {settings.MCP_SERVER_HOST}:{settings.MCP_SERVER_PORT}")
106
+ run_mcp_server(
107
+ transport="http",
108
+ host=settings.MCP_SERVER_HOST,
109
+ port=settings.MCP_SERVER_PORT,
110
+ )
111
+ else:
112
+ logger.info("Starting MCP server in local mode")
113
+ run_mcp_server()
114
+
115
+
116
+ logger = get_logger(__name__)
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
294
+ mcp = FastMCP(
295
+ name="mobile-use-mcp",
296
+ instructions="""
297
+ This server provides analysis tools for connected
298
+ mobile devices (iOS or Android).
299
+ Call get_available_devices() to list them.
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
308
+ )
309
+
310
+
311
+ @mcp.resource("data://devices")
312
+ def get_available_devices() -> list[DeviceInfo]:
313
+ """Provides a list of connected mobile devices (iOS or Android)."""
314
+ return list_available_devices()
315
+
316
+
317
+ def run_mcp_server(**mcp_run_kwargs):
318
+ """Run the MCP server with proper exception handling.
319
+
320
+ This wraps mcp.run() with exception handling for clean shutdown.
321
+ """
322
+ try:
323
+ mcp.run(**mcp_run_kwargs)
324
+ except KeyboardInterrupt:
325
+ logger.info("Keyboard interrupt received, shutting down...")
326
+ except Exception as e:
327
+ logger.error(f"Error running MCP server: {e}")
328
+ raise