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.
@@ -8,9 +8,12 @@ from minitap.mobile_use.sdk.types import ManualTaskConfig
8
8
  from minitap.mobile_use.sdk.types.task import PlatformTaskRequest
9
9
  from pydantic import Field
10
10
 
11
+ from minitap.mcp.core.cloud_apk import install_apk_on_cloud_mobile, upload_apk_to_cloud_mobile
12
+ from minitap.mcp.core.config import settings
11
13
  from minitap.mcp.core.decorators import handle_tool_errors
12
14
  from minitap.mcp.core.sdk_agent import get_mobile_use_agent
13
15
  from minitap.mcp.main import mcp
16
+ from minitap.mcp.server.cloud_mobile import check_cloud_mobile_status
14
17
 
15
18
 
16
19
  def _serialize_result(result: Any) -> Any:
@@ -26,14 +29,30 @@ def _serialize_result(result: Any) -> Any:
26
29
 
27
30
  @mcp.tool(
28
31
  name="execute_mobile_command",
29
- tags={"requires-maestro"},
30
32
  description="""
31
33
  Execute a natural language command on a mobile device using the Minitap SDK.
32
34
  This tool allows you to control your Android or iOS device using natural language.
35
+
33
36
  Examples:
34
37
  - "Open the settings app and tell me the battery level"
35
38
  - "Find the first 3 unread emails in Gmail"
36
39
  - "Take a screenshot and save it"
40
+
41
+ APK Deployment (Cloud Mobile Only):
42
+ When CLOUD_MOBILE_NAME is set, you can deploy and test APKs on cloud mobiles:
43
+ - Set apk_path to the path of your locally built APK
44
+ - The APK will be uploaded to cloud storage and installed on the device
45
+ - Requires MINITAP_API_KEY environment variable
46
+ - Must provide locked_app_package when using apk_path
47
+
48
+ Example with APK deployment:
49
+ execute_mobile_command(
50
+ apk_path="/path/to/app-debug.apk",
51
+ locked_app_package="com.example.myapp",
52
+ goal="Test the login flow with valid credentials"
53
+ )
54
+
55
+ Note: If apk path is set and no cloud mobile name -> it will raise a tool error
37
56
  """,
38
57
  )
39
58
  @handle_tool_errors
@@ -49,18 +68,44 @@ async def execute_mobile_command(
49
68
  default=None,
50
69
  description="Optional package name of the app to lock the device to. "
51
70
  "Will launch the app if not already running, and keep it in foreground "
52
- "until the task is completed.",
71
+ "until the task is completed. REQUIRED when using apk_path.",
72
+ ),
73
+ apk_path: str | None = Field(
74
+ default=None,
75
+ description="Path to local APK file to deploy to cloud mobile. "
76
+ "Only works when CLOUD_MOBILE_NAME is set. "
77
+ "The APK will be uploaded to cloud storage and installed before task execution. "
78
+ "Requires MINITAP_API_KEY to be configured. ",
53
79
  ),
54
80
  ) -> str | dict[str, Any]:
55
81
  """Run a manual task on a mobile device via the Minitap platform."""
56
82
  try:
83
+ if settings.CLOUD_MOBILE_NAME:
84
+ await check_cloud_mobile_status(settings.CLOUD_MOBILE_NAME)
85
+
86
+ if apk_path:
87
+ if not settings.CLOUD_MOBILE_NAME:
88
+ raise ToolError(
89
+ "apk_path parameter requires CLOUD_MOBILE_NAME to be set. "
90
+ "APK deployment is only supported in cloud mobile mode."
91
+ )
92
+
93
+ # Step 1: Upload APK via Platform storage API
94
+ filename = await upload_apk_to_cloud_mobile(apk_path=apk_path)
95
+
96
+ # Step 2: Install APK on cloud mobile
97
+ await install_apk_on_cloud_mobile(filename=filename)
98
+
57
99
  request = PlatformTaskRequest(
58
100
  task=ManualTaskConfig(
59
101
  goal=goal,
60
102
  output_description=output_description,
61
103
  ),
104
+ execution_origin="mcp",
62
105
  )
63
106
  agent = get_mobile_use_agent()
107
+ if not agent._initialized:
108
+ await agent.init()
64
109
  result = await agent.run_task(
65
110
  request=request,
66
111
  locked_app_package=locked_app_package,
@@ -0,0 +1,297 @@
1
+ """Tool for reading Swift/iOS logs for debugging during development."""
2
+
3
+ import asyncio
4
+ import json
5
+ import sys
6
+ from datetime import datetime
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from minitap.mcp.core.decorators import handle_tool_errors
11
+ from minitap.mcp.core.logging_config import get_logger
12
+ from minitap.mcp.main import mcp
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class BacktraceFrame(BaseModel):
18
+ imageOffset: int | None = None
19
+ imageUUID: str | None = None
20
+ imagePath: str | None = None
21
+ symbol: str | None = None
22
+
23
+
24
+ class Backtrace(BaseModel):
25
+ frames: list[BacktraceFrame] = []
26
+
27
+
28
+ class SimplifiedLog(BaseModel):
29
+ timestamp: str
30
+ level: str
31
+ category: str
32
+ message: str
33
+ process_id: int
34
+ backtrace: Backtrace | None = None
35
+ sender_image_path: str | None = None
36
+ process_image_path: str | None = None
37
+ sender_image_uuid: str | None = None
38
+
39
+
40
+ class LogsOutput(BaseModel):
41
+ bundle_id: str
42
+ last_minutes: int
43
+ log_count: int
44
+ logs: list[SimplifiedLog]
45
+ message: str | None = None
46
+
47
+
48
+ def _convert_to_iso8601(timestamp: str) -> str:
49
+ """Convert macOS log show timestamp to ISO8601 format.
50
+
51
+ Input format: "YYYY-MM-DD HH:MM:SS.NNNNNN±TTTT"
52
+ Output format: "YYYY-MM-DDTHH:MM:SS.NNNNNN±TT:TT"
53
+ """
54
+ if not timestamp:
55
+ return timestamp
56
+
57
+ try:
58
+ dt = datetime.fromisoformat(timestamp.replace(" ", "T"))
59
+ return dt.isoformat()
60
+ except ValueError:
61
+ return timestamp
62
+
63
+
64
+ def _parse_backtrace(raw: dict | None) -> Backtrace | None:
65
+ """Parse raw backtrace dict into Backtrace model."""
66
+ if not raw or not isinstance(raw, dict):
67
+ return None
68
+ frames_raw = raw.get("frames", [])
69
+ if not frames_raw:
70
+ return None
71
+ frames = [
72
+ BacktraceFrame(
73
+ imageOffset=f.get("imageOffset"),
74
+ imageUUID=f.get("imageUUID"),
75
+ imagePath=f.get("imagePath"),
76
+ symbol=f.get("symbol"),
77
+ )
78
+ for f in frames_raw
79
+ if isinstance(f, dict)
80
+ ]
81
+ return Backtrace(frames=frames) if frames else None
82
+
83
+
84
+ async def _run_log_show(
85
+ predicate: str | None,
86
+ last_minutes: int,
87
+ include_debug: bool,
88
+ *,
89
+ simulator: bool = False,
90
+ ) -> tuple[list, str | None]:
91
+ """Run log show command and return parsed logs and optional error message."""
92
+ if simulator:
93
+ cmd = ["xcrun", "simctl", "spawn", "booted", "log", "show"]
94
+ else:
95
+ cmd = ["log", "show"]
96
+
97
+ cmd.extend(["--style", "json", "--last", f"{last_minutes}m"])
98
+
99
+ if predicate:
100
+ cmd.extend(["--predicate", predicate])
101
+
102
+ if include_debug:
103
+ cmd.extend(["--debug", "--info"])
104
+
105
+ process = await asyncio.create_subprocess_exec(
106
+ *cmd,
107
+ stdout=asyncio.subprocess.PIPE,
108
+ stderr=asyncio.subprocess.PIPE,
109
+ )
110
+
111
+ stdout, stderr = await process.communicate()
112
+ error_output = stderr.decode("utf-8", errors="replace")
113
+
114
+ if process.returncode != 0:
115
+ if simulator and "No devices are booted" in error_output:
116
+ return [], "Error: No iOS Simulator is running. Please boot a simulator first."
117
+ return [], None
118
+
119
+ output = stdout.decode("utf-8", errors="replace").strip()
120
+ lines = output.split("\n")
121
+ if lines and lines[0].startswith("Filtering the log data"):
122
+ lines = lines[1:]
123
+ if lines and lines[0].startswith("Skipping info and debug"):
124
+ lines = lines[1:]
125
+
126
+ json_output = "\n".join(lines).strip()
127
+
128
+ if not json_output or json_output == "[]":
129
+ return [], None
130
+
131
+ try:
132
+ return json.loads(json_output), None
133
+ except json.JSONDecodeError:
134
+ return [], None
135
+
136
+
137
+ @mcp.tool(
138
+ name="read_swift_logs",
139
+ description="""
140
+ Read Swift/iOS logs for debugging during app development. Please note that this tool expect the
141
+ bundle identifier of the app to be passed as an argument.
142
+
143
+ This tool can read logs from:
144
+ 1. iOS Simulator runtime logs (source="simulator") - filters by process name
145
+ 2. All unified logging sources (source="all") - queries by subsystem and process name
146
+
147
+ Use cases:
148
+ - Debug runtime issues by reading simulator logs
149
+ - Find crash logs and error messages
150
+ - Read print() statements and os.Logger output from your Swift app
151
+
152
+ Examples:
153
+ - read_swift_logs(source="simulator", bundle_id="com.example.myapp")
154
+ - read_swift_logs(source="simulator", bundle_id="com.example.myapp", last_minutes=10)
155
+ - read_swift_logs(source="all", bundle_id="com.example.myapp", last_minutes=5)
156
+ """,
157
+ )
158
+ @handle_tool_errors
159
+ async def read_swift_logs(
160
+ bundle_id: str = Field(
161
+ description="The bundle identifier of the iOS app (e.g., 'com.example.myapp'). "
162
+ "This is used to filter logs by subsystem.",
163
+ ),
164
+ source: str = Field(
165
+ default="all",
166
+ description="Log source: 'simulator' for iOS Simulator runtime logs, "
167
+ "'all' to read from all sources that generate runtime logs related with the bundle.",
168
+ ),
169
+ last_minutes: int = Field(
170
+ default=5,
171
+ description="Number of minutes of logs to retrieve. Default is 5 minutes.",
172
+ ),
173
+ ) -> LogsOutput | str:
174
+ """Read Swift/iOS logs from simulator or file."""
175
+ if sys.platform != "darwin":
176
+ return "Error: This tool only works on macOS with Xcode installed."
177
+
178
+ process_name = bundle_id.split(".")[-1]
179
+
180
+ if source == "simulator":
181
+ return await _read_simulator_logs(bundle_id, last_minutes, process_name)
182
+ elif source == "all":
183
+ return await _read_file_logs(bundle_id, process_name, last_minutes)
184
+ else:
185
+ return f"Error: Unknown source '{source}'. Use 'simulator' or 'all'."
186
+
187
+
188
+ def _map_to_simplified_logs(log_entries: list[dict]) -> list[SimplifiedLog]:
189
+ return [
190
+ SimplifiedLog(
191
+ timestamp=_convert_to_iso8601(entry.get("timestamp", "")),
192
+ level=entry.get("messageType", ""),
193
+ category=entry.get("category", ""),
194
+ message=entry.get("eventMessage", ""),
195
+ process_id=entry.get("processID", 0),
196
+ backtrace=_parse_backtrace(entry.get("backtrace")),
197
+ sender_image_path=entry.get("senderImagePath"),
198
+ process_image_path=entry.get("processImagePath"),
199
+ sender_image_uuid=entry.get("senderImageUUID"),
200
+ )
201
+ for entry in log_entries
202
+ if entry.get("eventMessage")
203
+ ]
204
+
205
+
206
+ async def _read_simulator_logs(
207
+ bundle_id: str,
208
+ last_minutes: int,
209
+ process_name: str | None,
210
+ ) -> LogsOutput | str:
211
+ """Read historical logs from the booted iOS Simulator."""
212
+ predicate = f'processImagePath CONTAINS "{process_name}"' if process_name else None
213
+
214
+ logger.info(f"Reading simulator logs for last {last_minutes}m")
215
+
216
+ log_entries, error = await _run_log_show(
217
+ predicate, last_minutes, include_debug=True, simulator=True
218
+ )
219
+
220
+ if error:
221
+ return error
222
+
223
+ if not log_entries:
224
+ return LogsOutput(
225
+ bundle_id=bundle_id,
226
+ last_minutes=last_minutes,
227
+ log_count=0,
228
+ logs=[],
229
+ message=f"No logs found for '{process_name}' in the last {last_minutes} min.",
230
+ )
231
+
232
+ simplified_logs = _map_to_simplified_logs(log_entries)
233
+
234
+ return LogsOutput(
235
+ bundle_id=bundle_id,
236
+ last_minutes=last_minutes,
237
+ log_count=len(simplified_logs),
238
+ logs=simplified_logs,
239
+ )
240
+
241
+
242
+ async def _read_file_logs(bundle_id: str, process_name: str, last_minutes: int) -> LogsOutput:
243
+ # Query 1: Logs by subsystem (os.Logger logs)
244
+ subsystem_predicate = f'subsystem == "{bundle_id}"'
245
+
246
+ # Query 2: Logs by process name (catches crashes and system logs)
247
+ # Include fatal errors, crashes, and error-level logs
248
+ process_predicate = (
249
+ f'process == "{process_name}" AND '
250
+ f'(messageType == "Fault" OR messageType == "Error" OR '
251
+ f'eventMessage CONTAINS "fatal" OR eventMessage CONTAINS "crash")'
252
+ )
253
+
254
+ logger.info(
255
+ "fetching_ios_logs",
256
+ bundle_id=bundle_id,
257
+ last_minutes=last_minutes,
258
+ )
259
+
260
+ # Run both queries in parallel
261
+ (subsystem_logs, _), (process_logs, _) = await asyncio.gather(
262
+ _run_log_show(subsystem_predicate, last_minutes, include_debug=True),
263
+ _run_log_show(process_predicate, last_minutes, include_debug=False),
264
+ )
265
+
266
+ # Merge and deduplicate logs by timestamp + message
267
+ all_logs = subsystem_logs + process_logs
268
+ seen = set()
269
+ unique_logs = []
270
+ for log_entry in all_logs:
271
+ key = (log_entry.get("timestamp"), log_entry.get("eventMessage"))
272
+ if key not in seen:
273
+ seen.add(key)
274
+ unique_logs.append(log_entry)
275
+
276
+ # Sort by timestamp
277
+ unique_logs.sort(key=lambda x: x.get("timestamp", ""))
278
+
279
+ if not unique_logs:
280
+ return LogsOutput(
281
+ bundle_id=bundle_id,
282
+ last_minutes=last_minutes,
283
+ log_count=0,
284
+ logs=[],
285
+ message=f"No logs found for '{bundle_id}' in the last {last_minutes} min.",
286
+ )
287
+
288
+ simplified_logs = _map_to_simplified_logs(unique_logs)
289
+
290
+ logger.info("logs_retrieved", bundle_id=bundle_id, log_count=len(simplified_logs))
291
+
292
+ return LogsOutput(
293
+ bundle_id=bundle_id,
294
+ last_minutes=last_minutes,
295
+ log_count=len(simplified_logs),
296
+ logs=simplified_logs,
297
+ )
@@ -0,0 +1,53 @@
1
+ """Simple screenshot capture tool - returns raw base64 image without LLM analysis."""
2
+
3
+ import base64
4
+
5
+ from mcp.types import ImageContent
6
+ from pydantic import Field
7
+
8
+ from minitap.mcp.core.decorators import handle_tool_errors
9
+ from minitap.mcp.core.device import capture_screenshot, find_mobile_device
10
+ from minitap.mcp.main import mcp
11
+ from minitap.mcp.server.cloud_mobile import (
12
+ check_cloud_mobile_status,
13
+ get_cloud_mobile_id,
14
+ get_cloud_screenshot,
15
+ )
16
+
17
+
18
+ @mcp.tool(
19
+ name="take_screenshot",
20
+ description="""
21
+ Capture a screenshot from the connected mobile device.
22
+ Returns the raw base64-encoded PNG image directly without any LLM analysis.
23
+ Use this when you need the screenshot image for display or further processing.
24
+ """,
25
+ )
26
+ @handle_tool_errors
27
+ async def take_screenshot(
28
+ device_id: str | None = Field(
29
+ default=None,
30
+ description="ID of the device to capture screenshot from. "
31
+ "If not provided, the first available device is used.",
32
+ ),
33
+ ) -> list[ImageContent]:
34
+ """Capture screenshot and return as base64 image content."""
35
+ cloud_mobile_id = get_cloud_mobile_id()
36
+
37
+ if cloud_mobile_id:
38
+ # Cloud mode: use cloud screenshot API
39
+ await check_cloud_mobile_status(cloud_mobile_id)
40
+ screenshot_bytes = await get_cloud_screenshot(cloud_mobile_id)
41
+ screenshot_base64 = base64.b64encode(screenshot_bytes).decode("utf-8")
42
+ else:
43
+ # Local mode: capture from local device
44
+ device = find_mobile_device(device_id=device_id)
45
+ screenshot_base64 = capture_screenshot(device)
46
+
47
+ return [
48
+ ImageContent(
49
+ type="image",
50
+ data=screenshot_base64,
51
+ mimeType="image/png",
52
+ )
53
+ ]
@@ -0,0 +1,80 @@
1
+ """Tool for uploading device screenshots to remote storage.
2
+
3
+ This tool captures a screenshot from the connected device and uploads it
4
+ to remote storage, returning a filename that can be used with other tools
5
+ like figma_compare_screenshot.
6
+ """
7
+
8
+ import base64
9
+
10
+ from fastmcp.exceptions import ToolError
11
+ from fastmcp.tools.tool import ToolResult
12
+
13
+ from minitap.mcp.core.decorators import handle_tool_errors
14
+ from minitap.mcp.core.device import capture_screenshot, find_mobile_device
15
+ from minitap.mcp.core.logging_config import get_logger
16
+ from minitap.mcp.core.storage import StorageUploadError, upload_screenshot_to_storage
17
+ from minitap.mcp.main import mcp
18
+ from minitap.mcp.server.cloud_mobile import get_cloud_mobile_id, get_cloud_screenshot
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ @mcp.tool(
24
+ name="upload_screenshot",
25
+ description="""
26
+ Capture a screenshot from the connected device and upload it to storage.
27
+
28
+ This tool:
29
+ 1. Captures a screenshot from the connected device (local or cloud)
30
+ 2. Uploads the screenshot to remote storage
31
+ 3. Returns a filename that can be used with other tools
32
+
33
+ Use this to get a screenshot filename for tools like figma_compare_screenshot
34
+ that require a current_screenshot_filename parameter.
35
+
36
+ Example workflow:
37
+ 1. Call upload_screenshot to get a filename
38
+ 2. Use the returned filename with figma_compare_screenshot
39
+ """,
40
+ )
41
+ @handle_tool_errors
42
+ async def upload_screenshot() -> ToolResult:
43
+ """Capture and upload a device screenshot, return the filename."""
44
+ logger.info("Capturing and uploading device screenshot")
45
+
46
+ # Step 1: Capture screenshot from device
47
+ cloud_mobile_id = get_cloud_mobile_id()
48
+
49
+ if cloud_mobile_id:
50
+ logger.debug("Capturing screenshot from cloud device", device_id=cloud_mobile_id)
51
+ try:
52
+ screenshot_bytes = await get_cloud_screenshot(cloud_mobile_id)
53
+ screenshot_base64 = base64.b64encode(screenshot_bytes).decode("utf-8")
54
+ except Exception as e:
55
+ raise ToolError(f"Failed to capture cloud device screenshot: {e}") from e
56
+ else:
57
+ logger.debug("Capturing screenshot from local device")
58
+ try:
59
+ device = find_mobile_device()
60
+ screenshot_base64 = capture_screenshot(device)
61
+ except Exception as e:
62
+ raise ToolError(f"Failed to capture local device screenshot: {e}") from e
63
+
64
+ logger.info("Screenshot captured from device")
65
+
66
+ # Step 2: Upload screenshot to storage
67
+ try:
68
+ filename = await upload_screenshot_to_storage(screenshot_base64)
69
+ logger.info("Screenshot uploaded to storage", filename=filename)
70
+ except StorageUploadError as e:
71
+ raise ToolError(f"Failed to upload screenshot: {e}") from e
72
+
73
+ return ToolResult(
74
+ content=[
75
+ {
76
+ "type": "text",
77
+ "text": f"Screenshot uploaded successfully.\n\n**Filename:** {filename}",
78
+ }
79
+ ]
80
+ )
@@ -1,17 +1,19 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: minitap-mcp
3
- Version: 0.5.3
3
+ Version: 0.7.0
4
4
  Summary: Model Context Protocol server for controlling Android & iOS devices with natural language
5
5
  Author: Pierre-Louis Favreau, Jean-Pierre Lo, Clément Guiguet
6
6
  Requires-Dist: fastmcp>=2.12.4
7
7
  Requires-Dist: python-dotenv>=1.1.1
8
8
  Requires-Dist: pydantic>=2.12.0
9
9
  Requires-Dist: pydantic-settings>=2.10.1
10
- Requires-Dist: minitap-mobile-use>=2.9.3
10
+ Requires-Dist: minitap-mobile-use>=3.4.0
11
11
  Requires-Dist: jinja2>=3.1.6
12
12
  Requires-Dist: langchain-core>=0.3.75
13
13
  Requires-Dist: pillow>=11.1.0
14
14
  Requires-Dist: structlog>=24.4.0
15
+ Requires-Dist: aiohttp>=3.9.0
16
+ Requires-Dist: httpx>=0.28.1
15
17
  Requires-Dist: ruff==0.5.3 ; extra == 'dev'
16
18
  Requires-Dist: pytest==8.4.1 ; extra == 'dev'
17
19
  Requires-Dist: pytest-cov==5.0.0 ; extra == 'dev'
@@ -40,11 +42,19 @@ Before running the MCP server, ensure you have the required mobile automation to
40
42
  - **For Android devices:**
41
43
 
42
44
  - [ADB (Android Debug Bridge)](https://developer.android.com/tools/adb) - For device communication
43
- - [Maestro](https://maestro.mobile.dev/) - For mobile automation
44
45
 
45
46
  - **For iOS devices (macOS only):**
46
47
  - Xcode Command Line Tools with `xcrun`
47
- - [Maestro](https://maestro.mobile.dev/) - For mobile automation
48
+ - **[fb-idb](https://fbidb.io/docs/installation/)**: Facebook's iOS Development Bridge for device automation.
49
+
50
+ ```bash
51
+ # Install via Homebrew (macOS)
52
+ brew tap facebook/fb
53
+ brew install idb-companion
54
+ ```
55
+
56
+ > [!NOTE]
57
+ > `idb_companion` is required to communicate with iOS simulators. Make sure it's in your PATH after installation.
48
58
 
49
59
  For detailed setup instructions, see the [mobile-use repository](https://github.com/minitap-ai/mobile-use).
50
60
 
@@ -244,6 +254,50 @@ The tool will:
244
254
  3. Compare both screenshots using vision AI
245
255
  4. Return a detailed analysis highlighting differences
246
256
 
257
+ ## Cloud Mobile Mode
258
+
259
+ Run the MCP server with cloud-hosted mobile devices instead of requiring a local device. This enables:
260
+
261
+ - **Zero local setup**: No ADB or physical device required
262
+ - **Remote development**: Control cloud mobiles from anywhere
263
+ - **Scalable automation**: Access multiple cloud devices
264
+
265
+ ### Setting Up Cloud Mobile Mode
266
+
267
+ 1. **Create a Cloud Mobile** on [Minitap Platform](https://platform.minitap.ai/cloud-mobiles):
268
+ - Click **Create New Device**
269
+ - Choose platform (currently Android v11 / API level 30)
270
+ - Set a **Reference Name** (e.g., `my-dev-device`)
271
+
272
+ 2. **Configure the environment variable**:
273
+
274
+ ```bash
275
+ # Using reference name (recommended)
276
+ export CLOUD_MOBILE_NAME="my-dev-device"
277
+ ```
278
+
279
+ 3. **Start the server** (no local device needed):
280
+
281
+ ```bash
282
+ minitap-mcp --server --api-key your_minitap_api_key
283
+ ```
284
+
285
+ The server will:
286
+ - Connect to your cloud mobile on startup
287
+ - Maintain a keep-alive connection while running
288
+ - Automatically disconnect when the server stops
289
+
290
+ > ⚠️ **Important**: Cloud mobiles are billed while connected. The MCP server automatically stops the connection when you close your IDE or stop the server. Make sure to properly shut down the server to avoid unexpected charges.
291
+
292
+ ### Cloud vs Local Mode
293
+
294
+ | Feature | Local Mode | Cloud Mode |
295
+ |---------|------------|------------|
296
+ | Device requirement | Physical/emulator | None |
297
+ | Setup complexity | ADB setup required | Low (env var only) |
298
+ | `CLOUD_MOBILE_NAME` | Not set | Set to device name/UUID |
299
+ | Billing | None | Per-minute usage |
300
+
247
301
  ## Advanced Configuration
248
302
 
249
303
  ### Custom ADB Server
@@ -259,7 +313,7 @@ export ADB_SERVER_SOCKET="tcp:192.168.1.100:5037"
259
313
  Customize the vision model used for screen analysis:
260
314
 
261
315
  ```bash
262
- export VISION_MODEL="qwen/qwen-2.5-vl-7b-instruct"
316
+ export VISION_MODEL="google/gemini-3-flash-preview"
263
317
  ```
264
318
 
265
319
  ## Device Setup
@@ -0,0 +1,34 @@
1
+ minitap/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ minitap/mcp/core/agents/compare_screenshots/agent.py,sha256=XRYyrv9QA4eb1b44hPjAqz6MKYM6y3THosxP21xzBY4,2458
3
+ minitap/mcp/core/agents/compare_screenshots/eval/prompts/prompt_1.md,sha256=qAyqOroSJROgrvlbsLCtiwFyBKuIMCQ-720A5cwgwPY,3563
4
+ minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/actual.png,sha256=woKE-aTTdb-9ArqfnV-xKKyit1Fu_hklavVwAaMQ14E,99455
5
+ minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/figma.png,sha256=ghxi1P-ofnmMv5_ASm0Rzo5ll_C8-E6ojQUCHRR33TA,102098
6
+ minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/human_feedback.txt,sha256=0gVEqIFpmCX1cx1tlGBdfqDGbTeedyVfwTJZqePURkA,700
7
+ minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/model_params.json,sha256=p93kbUGxLYwc4NAEzPTBDA2XZ1ucsa7yevXZRe6V4Mc,37
8
+ minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/output.md,sha256=Q0_5w9x5WAMqx6-I7KgjlS-tt7HnWKIunP43J7F0W_o,3703
9
+ minitap/mcp/core/agents/compare_screenshots/prompt.md,sha256=qAyqOroSJROgrvlbsLCtiwFyBKuIMCQ-720A5cwgwPY,3563
10
+ minitap/mcp/core/cloud_apk.py,sha256=Xeg1jrycm50UC72J4qVZ2NS9qs20oXKhvBcmBRs4rEA,3896
11
+ minitap/mcp/core/config.py,sha256=aN2x4Qj5lSmwCCxH03Si6-pZ7oIVKOTBKF4itfg1ifM,1408
12
+ minitap/mcp/core/decorators.py,sha256=ipzR7kbMXacG91f6CliN-nl9unRTtjmANrfueaOXJ2s,3591
13
+ minitap/mcp/core/device.py,sha256=0AU8qGi26axC6toqHrPIzNeDbNDtll0YRwkspHouPmM,8198
14
+ minitap/mcp/core/llm.py,sha256=tI5m5rFDLeMkXE5WExnzYSzHU3nTIEiSC9nAsPzVMaU,1144
15
+ minitap/mcp/core/logging_config.py,sha256=OJlArPJxflbhckerFsRHVTzy3jwsLsNSPN0LVpkmpNM,1861
16
+ minitap/mcp/core/models.py,sha256=egLScxPAMo4u5cqY33UKba7z7DsdgqfPW409UAqW1Jg,1942
17
+ minitap/mcp/core/sdk_agent.py,sha256=5YEr7aDjoiwbRQkZBK3jDa08c5QhPwLxahzlYrEB_KE,1132
18
+ minitap/mcp/core/storage.py,sha256=y92pnE0pfLtH4nDKHZeUAZm_udR5sbRQGWbw13mjd_U,8189
19
+ minitap/mcp/core/utils/figma.py,sha256=L5aAHm59mrRYaqrwMJSM24SSdZPu2yVg-wsHTF3L8vk,2310
20
+ minitap/mcp/core/utils/images.py,sha256=3uExpRoh7affIieZx3TLlZTmZCcoxWfx1YpPbwhjiJY,1791
21
+ minitap/mcp/main.py,sha256=p5E_dBIBiS_sp_v9u8gNxjRGwl25T4De-ivZQbSqJa8,11269
22
+ minitap/mcp/server/cloud_mobile.py,sha256=4cKnUzOJE5tBzgXgJXmyH0ESf6OyCTlNllputEQwFvE,18261
23
+ minitap/mcp/server/middleware.py,sha256=SjPc4pcfPuG0TnaDH7a19DS_HRFPl3bkbovdOLzy_IU,768
24
+ minitap/mcp/server/poller.py,sha256=JsdW6nvj4r3tsn8AaTwXD4H9dVAAau4BhJXHXHit9nA,2528
25
+ minitap/mcp/server/remote_proxy.py,sha256=IM7UfjbJlQRpFD_tdpdck1mFT1QOnlxj5OA1nS4tRhQ,3073
26
+ minitap/mcp/tools/execute_mobile_command.py,sha256=YXHLKvNFmuvwh7d_kPYw0R64bHzKCjyqp38adx8jWWI,4453
27
+ minitap/mcp/tools/read_swift_logs.py,sha256=Wc1XqQWWuNuPEIBioYD2geVd1p9Yq2USik6SX47Fq9A,9285
28
+ minitap/mcp/tools/screen_analyzer.md,sha256=TTO80JQWusbA9cKAZn-9cqhgVHm6F_qJh5w152hG3YM,734
29
+ minitap/mcp/tools/take_screenshot.py,sha256=gGySPSeVnx8lHiseGF_Wat82JLF-D8GuQIJ_hCaLZlQ,1730
30
+ minitap/mcp/tools/upload_screenshot.py,sha256=kwh8Q46LWF3nyKbKlvnlf-CtGrPkctXSnLyeebQGNFI,2959
31
+ minitap_mcp-0.7.0.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
32
+ minitap_mcp-0.7.0.dist-info/entry_points.txt,sha256=rYVoXm7tSQCqQTtHx4Lovgn1YsjwtEEHfddKrfEVHuY,55
33
+ minitap_mcp-0.7.0.dist-info/METADATA,sha256=1WRLCep-7AjDFfhBEUNwZToFWzb26Tml1XafKjWBvuE,10619
34
+ minitap_mcp-0.7.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.10
2
+ Generator: uv 0.9.26
3
3
  Root-Is-Purelib: true
4
- Tag: py3-none-any
4
+ Tag: py3-none-any