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.
- minitap/mcp/core/config.py +2 -4
- minitap/mcp/core/decorators.py +19 -1
- minitap/mcp/core/device.py +11 -4
- minitap/mcp/core/storage.py +274 -0
- minitap/mcp/main.py +57 -5
- minitap/mcp/server/cloud_mobile.py +95 -0
- minitap/mcp/server/remote_proxy.py +96 -0
- minitap/mcp/tools/execute_mobile_command.py +4 -0
- 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.6.0.dist-info → minitap_mcp-0.7.0.dist-info}/METADATA +4 -3
- {minitap_mcp-0.6.0.dist-info → minitap_mcp-0.7.0.dist-info}/RECORD +15 -13
- {minitap_mcp-0.6.0.dist-info → minitap_mcp-0.7.0.dist-info}/WHEEL +2 -2
- minitap/mcp/tools/analyze_screen.py +0 -69
- minitap/mcp/tools/compare_screenshot_with_figma.py +0 -132
- minitap/mcp/tools/save_figma_assets.py +0 -276
- {minitap_mcp-0.6.0.dist-info → minitap_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -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.
|
|
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.
|
|
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="
|
|
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=
|
|
12
|
-
minitap/mcp/core/decorators.py,sha256=
|
|
13
|
-
minitap/mcp/core/device.py,sha256=
|
|
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=
|
|
21
|
-
minitap/mcp/server/cloud_mobile.py,sha256=
|
|
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/
|
|
25
|
-
minitap/mcp/tools/
|
|
26
|
-
minitap/mcp/tools/
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
minitap_mcp-0.
|
|
32
|
-
minitap_mcp-0.
|
|
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,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
|