minitap-mcp 0.6.0__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.
@@ -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,18 +1,19 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: minitap-mcp
3
- Version: 0.6.0
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>=3.0.0
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
15
  Requires-Dist: aiohttp>=3.9.0
16
+ Requires-Dist: httpx>=0.28.1
16
17
  Requires-Dist: ruff==0.5.3 ; extra == 'dev'
17
18
  Requires-Dist: pytest==8.4.1 ; extra == 'dev'
18
19
  Requires-Dist: pytest-cov==5.0.0 ; extra == 'dev'
@@ -312,7 +313,7 @@ export ADB_SERVER_SOCKET="tcp:192.168.1.100:5037"
312
313
  Customize the vision model used for screen analysis:
313
314
 
314
315
  ```bash
315
- export VISION_MODEL="qwen/qwen-2.5-vl-7b-instruct"
316
+ export VISION_MODEL="google/gemini-3-flash-preview"
316
317
  ```
317
318
 
318
319
  ## Device Setup
@@ -8,25 +8,27 @@ minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_
8
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
9
  minitap/mcp/core/agents/compare_screenshots/prompt.md,sha256=qAyqOroSJROgrvlbsLCtiwFyBKuIMCQ-720A5cwgwPY,3563
10
10
  minitap/mcp/core/cloud_apk.py,sha256=Xeg1jrycm50UC72J4qVZ2NS9qs20oXKhvBcmBRs4rEA,3896
11
- minitap/mcp/core/config.py,sha256=v_neP4lqIx7sigiAPxUALZg5QBrIx-N4mJ848YRfG84,1428
12
- minitap/mcp/core/decorators.py,sha256=kMx_mlaa-2U1AgCoYkgPoLOa-iOoKUF1OjcNV7x59Ds,2940
13
- minitap/mcp/core/device.py,sha256=sEO3Z-8F325hDOObdH1YBhZE60f17FmIclt5UlhY_nU,7875
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
14
  minitap/mcp/core/llm.py,sha256=tI5m5rFDLeMkXE5WExnzYSzHU3nTIEiSC9nAsPzVMaU,1144
15
15
  minitap/mcp/core/logging_config.py,sha256=OJlArPJxflbhckerFsRHVTzy3jwsLsNSPN0LVpkmpNM,1861
16
16
  minitap/mcp/core/models.py,sha256=egLScxPAMo4u5cqY33UKba7z7DsdgqfPW409UAqW1Jg,1942
17
17
  minitap/mcp/core/sdk_agent.py,sha256=5YEr7aDjoiwbRQkZBK3jDa08c5QhPwLxahzlYrEB_KE,1132
18
+ minitap/mcp/core/storage.py,sha256=y92pnE0pfLtH4nDKHZeUAZm_udR5sbRQGWbw13mjd_U,8189
18
19
  minitap/mcp/core/utils/figma.py,sha256=L5aAHm59mrRYaqrwMJSM24SSdZPu2yVg-wsHTF3L8vk,2310
19
20
  minitap/mcp/core/utils/images.py,sha256=3uExpRoh7affIieZx3TLlZTmZCcoxWfx1YpPbwhjiJY,1791
20
- minitap/mcp/main.py,sha256=6UjO26otULFbTmoI_WziknaRyBTXSazH9sqXRP-wns8,9250
21
- minitap/mcp/server/cloud_mobile.py,sha256=kntcdMkc89QoXyLc-f5bzWFm6UBj52NqrBNHopAv-ag,14573
21
+ minitap/mcp/main.py,sha256=p5E_dBIBiS_sp_v9u8gNxjRGwl25T4De-ivZQbSqJa8,11269
22
+ minitap/mcp/server/cloud_mobile.py,sha256=4cKnUzOJE5tBzgXgJXmyH0ESf6OyCTlNllputEQwFvE,18261
22
23
  minitap/mcp/server/middleware.py,sha256=SjPc4pcfPuG0TnaDH7a19DS_HRFPl3bkbovdOLzy_IU,768
23
24
  minitap/mcp/server/poller.py,sha256=JsdW6nvj4r3tsn8AaTwXD4H9dVAAau4BhJXHXHit9nA,2528
24
- minitap/mcp/tools/analyze_screen.py,sha256=_7tj7AJvopotvItGIN55OoztJXmVu3j2ZsBmYq28pVw,2422
25
- minitap/mcp/tools/compare_screenshot_with_figma.py,sha256=gMaYItE_bg_EwY2-Ux9RQd-Cnz6s2RCtYh1Q5n7HciU,4621
26
- minitap/mcp/tools/execute_mobile_command.py,sha256=e49Y14KLTJ63ZQvLUrn-CvC5TQrylCXsUDEuJOTE5j8,4271
27
- minitap/mcp/tools/save_figma_assets.py,sha256=V1gnQsJ1tciOxiK08aaqQxOEerJkKzxU8r4hJmkXHtA,9945
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
28
  minitap/mcp/tools/screen_analyzer.md,sha256=TTO80JQWusbA9cKAZn-9cqhgVHm6F_qJh5w152hG3YM,734
29
- minitap_mcp-0.6.0.dist-info/WHEEL,sha256=93kfTGt3a0Dykt_T-gsjtyS5_p8F_d6CE1NwmBOirzo,79
30
- minitap_mcp-0.6.0.dist-info/entry_points.txt,sha256=rYVoXm7tSQCqQTtHx4Lovgn1YsjwtEEHfddKrfEVHuY,55
31
- minitap_mcp-0.6.0.dist-info/METADATA,sha256=pzp6FGqqncEUQ9YNyheBLGonB2Pe8DDE2LKJRwJ-HE4,10589
32
- minitap_mcp-0.6.0.dist-info/RECORD,,
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.16
2
+ Generator: uv 0.9.26
3
3
  Root-Is-Purelib: true
4
- Tag: py3-none-any
4
+ Tag: py3-none-any
@@ -1,69 +0,0 @@
1
- import base64
2
- from pathlib import Path
3
- from uuid import uuid4
4
-
5
- from jinja2 import Template
6
- from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
7
- from pydantic import Field
8
-
9
- from minitap.mcp.core.config import settings
10
- from minitap.mcp.core.decorators import handle_tool_errors
11
- from minitap.mcp.core.device import capture_screenshot, find_mobile_device
12
- from minitap.mcp.core.llm import get_minitap_llm
13
- from minitap.mcp.core.utils.images import compress_base64_jpeg, get_screenshot_message_for_llm
14
- from minitap.mcp.main import mcp
15
- from minitap.mcp.server.cloud_mobile import get_cloud_mobile_id, get_cloud_screenshot
16
-
17
-
18
- @mcp.tool(
19
- name="analyze_screen",
20
- description="""
21
- Analyze what is shown on the mobile device screen.
22
- This tool takes a screenshot file path and uses a vision-capable LLM
23
- to analyze and describe what's on the screen. Useful for understanding
24
- UI elements, extracting text, or identifying specific features.
25
- """,
26
- )
27
- @handle_tool_errors
28
- async def analyze_screen(
29
- prompt: str = Field(
30
- description="Prompt for the analysis.",
31
- ),
32
- device_id: str | None = Field(
33
- default=None,
34
- description="ID of the device screen to analyze. "
35
- "If not provided, the first available device is taken.",
36
- ),
37
- ) -> str | list | dict:
38
- system_message = Template(
39
- Path(__file__).parent.joinpath("screen_analyzer.md").read_text(encoding="utf-8")
40
- ).render()
41
-
42
- # Check if running in cloud mode
43
- cloud_mobile_id = get_cloud_mobile_id()
44
-
45
- if cloud_mobile_id:
46
- # Cloud mode: fetch screenshot from DaaS API
47
- screenshot_bytes = await get_cloud_screenshot(cloud_mobile_id)
48
- screenshot_base64 = base64.b64encode(screenshot_bytes).decode("utf-8")
49
- else:
50
- # Local mode: capture from local device
51
- device = find_mobile_device(device_id=device_id)
52
- screenshot_base64 = capture_screenshot(device)
53
-
54
- compressed_image_base64 = compress_base64_jpeg(screenshot_base64)
55
-
56
- messages: list[BaseMessage] = [
57
- SystemMessage(content=system_message),
58
- get_screenshot_message_for_llm(compressed_image_base64),
59
- HumanMessage(content=prompt),
60
- ]
61
-
62
- llm = get_minitap_llm(
63
- trace_id=str(uuid4()),
64
- remote_tracing=True,
65
- model=settings.VISION_MODEL,
66
- temperature=1,
67
- )
68
- response = await llm.ainvoke(messages)
69
- return response.content