minitap-mcp 0.8.0__tar.gz → 0.9.0__tar.gz
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-0.8.0 → minitap_mcp-0.9.0}/PKG-INFO +2 -2
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/cloud_apk.py +9 -1
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/config.py +29 -3
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/storage.py +133 -0
- minitap_mcp-0.9.0/minitap/mcp/core/task_runs.py +100 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/tools/execute_mobile_command.py +69 -2
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/pyproject.toml +2 -2
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/PYPI_README.md +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/__init__.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/agent.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/eval/prompts/prompt_1.md +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/actual.png +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/figma.png +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/human_feedback.txt +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/model_params.json +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/output.md +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/prompt.md +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/decorators.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/device.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/llm.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/logging_config.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/models.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/sdk_agent.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/utils/figma.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/utils/images.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/main.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/server/cloud_mobile.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/server/middleware.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/server/poller.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/server/remote_proxy.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/tools/read_swift_logs.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/tools/screen_analyzer.md +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/tools/take_screenshot.py +0 -0
- {minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/tools/upload_screenshot.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: minitap-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -32,7 +32,15 @@ async def upload_apk_to_cloud_mobile(apk_path: str) -> str:
|
|
|
32
32
|
if not apk_file.exists():
|
|
33
33
|
raise FileNotFoundError(f"APK file not found: {apk_path}")
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# Use APP_PACKAGE_NAME env var if set, otherwise generate a random name
|
|
36
|
+
# Preserve the original file extension
|
|
37
|
+
extension = apk_file.suffix # e.g., ".apk"
|
|
38
|
+
if settings.APP_PACKAGE_NAME:
|
|
39
|
+
# Strip any existing extension from APP_PACKAGE_NAME to avoid double extensions
|
|
40
|
+
base_name = Path(settings.APP_PACKAGE_NAME).stem
|
|
41
|
+
filename = f"{base_name}{extension}"
|
|
42
|
+
else:
|
|
43
|
+
filename = f"app_{uuid.uuid4().hex[:6]}{extension}"
|
|
36
44
|
api_key = settings.MINITAP_API_KEY.get_secret_value()
|
|
37
45
|
api_base_url = settings.MINITAP_API_BASE_URL.rstrip("/")
|
|
38
46
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Configuration for the MCP server."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from urllib.parse import urlparse
|
|
4
5
|
|
|
5
6
|
from dotenv import load_dotenv
|
|
@@ -14,22 +15,32 @@ def _derive_mcp_url_from_base(base_url: str) -> str:
|
|
|
14
15
|
"""Derive the MCP URL from the API base URL.
|
|
15
16
|
|
|
16
17
|
Extracts the scheme and host from the base URL and appends /api/mcp.
|
|
17
|
-
Example: https://dev.platform.minitap.ai/api/v1 -> https://dev.platform.minitap.ai/api/mcp
|
|
18
|
+
Example: https://dev.platform.minitap.ai/api/v1 -> https://dev.platform.minitap.ai/api/mcp/
|
|
18
19
|
"""
|
|
19
20
|
parsed = urlparse(base_url)
|
|
20
|
-
return f"{parsed.scheme}://{parsed.netloc}/api/mcp"
|
|
21
|
+
return f"{parsed.scheme}://{parsed.netloc}/api/mcp/"
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def _derive_daas_url_from_base(base_url: str) -> str:
|
|
24
25
|
"""Derive the DaaS API URL from the API base URL.
|
|
25
26
|
|
|
26
27
|
Extracts the scheme and host from the base URL and appends /api/daas.
|
|
27
|
-
Example: https://dev.platform.minitap.ai/api/v1 -> https://dev.platform.minitap.ai/api/daas
|
|
28
|
+
Example: https://dev.platform.minitap.ai/api/v1 -> https://dev.platform.minitap.ai/api/daas/
|
|
28
29
|
"""
|
|
29
30
|
parsed = urlparse(base_url)
|
|
30
31
|
return f"{parsed.scheme}://{parsed.netloc}/api/daas"
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
def _derive_platform_base_url(api_base_url: str) -> str:
|
|
35
|
+
"""Derive the platform base URL from the API base URL.
|
|
36
|
+
|
|
37
|
+
Extracts the scheme and host from the API URL (strips /api/v1 path).
|
|
38
|
+
Example: https://dev.platform.minitap.ai/api/v1 -> https://dev.platform.minitap.ai
|
|
39
|
+
"""
|
|
40
|
+
parsed = urlparse(api_base_url)
|
|
41
|
+
return f"{parsed.scheme}://{parsed.netloc}"
|
|
42
|
+
|
|
43
|
+
|
|
33
44
|
class MCPSettings(BaseSettings):
|
|
34
45
|
"""Configuration class for MCP server."""
|
|
35
46
|
|
|
@@ -57,6 +68,16 @@ class MCPSettings(BaseSettings):
|
|
|
57
68
|
# Create cloud mobiles at https://platform.minitap.ai/cloud-mobiles
|
|
58
69
|
CLOUD_MOBILE_NAME: str | None = Field(default=None)
|
|
59
70
|
|
|
71
|
+
# Trajectory GIF download configuration
|
|
72
|
+
# When set, downloads the trajectory GIF after task execution to the specified folder.
|
|
73
|
+
# The folder is a directory where the GIF will be saved with the task run ID as filename.
|
|
74
|
+
TRAJECTORY_GIF_DOWNLOAD_FOLDER: str | None = Field(default=None)
|
|
75
|
+
|
|
76
|
+
# App package name override for uploads
|
|
77
|
+
# When set, uploaded APK/IPA files will use this name instead of a random UUID.
|
|
78
|
+
# The original file extension is preserved. Example: "my_app" -> "my_app.apk"
|
|
79
|
+
APP_PACKAGE_NAME: str | None = Field(default=None)
|
|
80
|
+
|
|
60
81
|
@model_validator(mode="after")
|
|
61
82
|
def derive_urls_from_base(self) -> "MCPSettings":
|
|
62
83
|
"""Derive MCP and DaaS URLs from base URL if not explicitly set.
|
|
@@ -79,6 +100,11 @@ class MCPSettings(BaseSettings):
|
|
|
79
100
|
_derive_daas_url_from_base(self.MINITAP_API_BASE_URL),
|
|
80
101
|
)
|
|
81
102
|
|
|
103
|
+
# Set MINITAP_BASE_URL in environment for mobile-use SDK compatibility.
|
|
104
|
+
# The SDK uses MINITAP_BASE_URL (e.g., https://dev.platform.minitap.ai) while
|
|
105
|
+
# MCP uses MINITAP_API_BASE_URL (e.g., https://dev.platform.minitap.ai/api/v1).
|
|
106
|
+
os.environ["MINITAP_BASE_URL"] = _derive_platform_base_url(self.MINITAP_API_BASE_URL)
|
|
107
|
+
|
|
82
108
|
return self
|
|
83
109
|
|
|
84
110
|
|
|
@@ -272,3 +272,136 @@ def _guess_mime_type(extension: str) -> str:
|
|
|
272
272
|
"txt": "text/plain",
|
|
273
273
|
}
|
|
274
274
|
return ext_to_mime.get(ext, "application/octet-stream")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class StorageDownloadError(Exception):
|
|
278
|
+
"""Error raised when file download fails."""
|
|
279
|
+
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
async def get_trajectory_gif_download_url(task_run_id: str) -> str:
|
|
284
|
+
"""Get a signed download URL for a trajectory GIF.
|
|
285
|
+
|
|
286
|
+
This function calls the MaaS API to get a signed S3 download URL for the
|
|
287
|
+
trajectory GIF associated with a task run.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
task_run_id: The ID of the task run to get the GIF for
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
The signed download URL for the GIF
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
StorageDownloadError: If the request fails or no URL is returned
|
|
297
|
+
"""
|
|
298
|
+
try:
|
|
299
|
+
api_key = _get_api_key()
|
|
300
|
+
except StorageUploadError as e:
|
|
301
|
+
raise StorageDownloadError(str(e)) from e
|
|
302
|
+
base_url = settings.MINITAP_API_BASE_URL
|
|
303
|
+
endpoint = f"{base_url}/storage/trajectory-gif-download/{task_run_id}"
|
|
304
|
+
|
|
305
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
306
|
+
try:
|
|
307
|
+
logger.debug("Requesting trajectory GIF download URL", task_run_id=task_run_id)
|
|
308
|
+
response = await client.get(
|
|
309
|
+
endpoint,
|
|
310
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if response.status_code == 404:
|
|
314
|
+
raise StorageDownloadError(f"Trajectory GIF not found for task run: {task_run_id}")
|
|
315
|
+
|
|
316
|
+
if response.status_code != 200:
|
|
317
|
+
logger.error(
|
|
318
|
+
"Failed to get trajectory GIF download URL",
|
|
319
|
+
status_code=response.status_code,
|
|
320
|
+
response=response.text,
|
|
321
|
+
)
|
|
322
|
+
raise StorageDownloadError(
|
|
323
|
+
f"Failed to get trajectory GIF download URL: HTTP {response.status_code}"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
data = response.json()
|
|
327
|
+
download_url = data.get("signed_url")
|
|
328
|
+
if not download_url:
|
|
329
|
+
raise StorageDownloadError("No download URL returned in response")
|
|
330
|
+
|
|
331
|
+
logger.debug("Got trajectory GIF download URL", task_run_id=task_run_id)
|
|
332
|
+
return download_url
|
|
333
|
+
|
|
334
|
+
except httpx.TimeoutException as e:
|
|
335
|
+
logger.error("Trajectory GIF download URL request timed out", error=str(e))
|
|
336
|
+
raise StorageDownloadError("Request timed out") from e
|
|
337
|
+
except httpx.RequestError as e:
|
|
338
|
+
logger.error("Trajectory GIF download URL request failed", error=str(e))
|
|
339
|
+
raise StorageDownloadError(f"Request failed: {str(e)}") from e
|
|
340
|
+
except StorageDownloadError:
|
|
341
|
+
raise
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.error("Unexpected error getting trajectory GIF download URL", error=str(e))
|
|
344
|
+
raise StorageDownloadError(f"Unexpected error: {str(e)}") from e
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
async def download_trajectory_gif(task_run_id: str, download_path: str | Path) -> Path:
|
|
348
|
+
"""Download a trajectory GIF to a local path.
|
|
349
|
+
|
|
350
|
+
This function:
|
|
351
|
+
1. Gets a signed download URL from the MaaS API
|
|
352
|
+
2. Downloads the GIF from that URL
|
|
353
|
+
3. Saves it to the specified local path
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
task_run_id: The ID of the task run to download the GIF for
|
|
357
|
+
download_path: Directory path where the GIF will be saved.
|
|
358
|
+
The file will be saved as {task_run_id}/trajectory.gif
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
The full path to the downloaded GIF file
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
StorageDownloadError: If download fails at any step
|
|
365
|
+
"""
|
|
366
|
+
download_dir = Path(download_path) / task_run_id
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
download_dir.mkdir(parents=True, exist_ok=True)
|
|
370
|
+
except OSError as e:
|
|
371
|
+
raise StorageDownloadError(f"Failed to create download directory: {e}") from e
|
|
372
|
+
|
|
373
|
+
download_url = await get_trajectory_gif_download_url(task_run_id)
|
|
374
|
+
|
|
375
|
+
output_file = download_dir / "trajectory.gif"
|
|
376
|
+
|
|
377
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
378
|
+
try:
|
|
379
|
+
logger.info(
|
|
380
|
+
"Downloading trajectory GIF", task_run_id=task_run_id, path=str(output_file)
|
|
381
|
+
)
|
|
382
|
+
response = await client.get(download_url)
|
|
383
|
+
|
|
384
|
+
if response.status_code != 200:
|
|
385
|
+
raise StorageDownloadError(f"Failed to download GIF: HTTP {response.status_code}")
|
|
386
|
+
|
|
387
|
+
output_file.write_bytes(response.content)
|
|
388
|
+
logger.info(
|
|
389
|
+
"Trajectory GIF downloaded successfully",
|
|
390
|
+
task_run_id=task_run_id,
|
|
391
|
+
path=str(output_file),
|
|
392
|
+
size=len(response.content),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
return output_file
|
|
396
|
+
|
|
397
|
+
except httpx.TimeoutException as e:
|
|
398
|
+
logger.error("GIF download timed out", error=str(e))
|
|
399
|
+
raise StorageDownloadError("Download timed out") from e
|
|
400
|
+
except httpx.RequestError as e:
|
|
401
|
+
logger.error("GIF download request failed", error=str(e))
|
|
402
|
+
raise StorageDownloadError(f"Download failed: {str(e)}") from e
|
|
403
|
+
except StorageDownloadError:
|
|
404
|
+
raise
|
|
405
|
+
except Exception as e:
|
|
406
|
+
logger.error("Unexpected error downloading GIF", error=str(e))
|
|
407
|
+
raise StorageDownloadError(f"Unexpected error: {str(e)}") from e
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Task runs API utilities.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to interact with the task runs API
|
|
4
|
+
for fetching information about executed tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from minitap.mcp.core.config import settings
|
|
10
|
+
from minitap.mcp.core.logging_config import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TaskRunsError(Exception):
|
|
16
|
+
"""Error raised when task runs API operations fail."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_api_key() -> str:
|
|
22
|
+
"""Get the API key from settings.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The API key string
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
TaskRunsError: If API key is not configured
|
|
29
|
+
"""
|
|
30
|
+
api_key = settings.MINITAP_API_KEY.get_secret_value() if settings.MINITAP_API_KEY else None
|
|
31
|
+
if not api_key:
|
|
32
|
+
raise TaskRunsError("MINITAP_API_KEY is required for task runs API")
|
|
33
|
+
return api_key
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def get_latest_task_run_id() -> str:
|
|
37
|
+
"""Get the ID of the most recently finished task run.
|
|
38
|
+
|
|
39
|
+
This function calls the MaaS API to get the latest task run,
|
|
40
|
+
sorted by finished_at in descending order.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The ID of the latest task run
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
TaskRunsError: If the request fails or no task run is found
|
|
47
|
+
"""
|
|
48
|
+
api_key = _get_api_key()
|
|
49
|
+
base_url = settings.MINITAP_API_BASE_URL
|
|
50
|
+
endpoint = f"{base_url}/task-runs"
|
|
51
|
+
|
|
52
|
+
params = {
|
|
53
|
+
"page": 1,
|
|
54
|
+
"pageSize": 1,
|
|
55
|
+
"orphans": "include",
|
|
56
|
+
"virtualMobile": "include",
|
|
57
|
+
"sortBy": "finished_at",
|
|
58
|
+
"sortOrder": "desc",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
62
|
+
try:
|
|
63
|
+
logger.debug("Fetching latest task run ID")
|
|
64
|
+
response = await client.get(
|
|
65
|
+
endpoint,
|
|
66
|
+
params=params,
|
|
67
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if response.status_code != 200:
|
|
71
|
+
logger.error(
|
|
72
|
+
"Failed to get latest task run",
|
|
73
|
+
status_code=response.status_code,
|
|
74
|
+
response=response.text,
|
|
75
|
+
)
|
|
76
|
+
raise TaskRunsError(f"Failed to get latest task run: HTTP {response.status_code}")
|
|
77
|
+
|
|
78
|
+
data = response.json()
|
|
79
|
+
items = data.get("runs", [])
|
|
80
|
+
if not items:
|
|
81
|
+
raise TaskRunsError("No task runs found")
|
|
82
|
+
|
|
83
|
+
task_run_id = items[0].get("id")
|
|
84
|
+
if not task_run_id:
|
|
85
|
+
raise TaskRunsError("Task run ID not found in response")
|
|
86
|
+
|
|
87
|
+
logger.debug("Got latest task run ID", task_run_id=task_run_id)
|
|
88
|
+
return task_run_id
|
|
89
|
+
|
|
90
|
+
except httpx.TimeoutException as e:
|
|
91
|
+
logger.error("Latest task run request timed out", error=str(e))
|
|
92
|
+
raise TaskRunsError("Request timed out") from e
|
|
93
|
+
except httpx.RequestError as e:
|
|
94
|
+
logger.error("Latest task run request failed", error=str(e))
|
|
95
|
+
raise TaskRunsError(f"Request failed: {str(e)}") from e
|
|
96
|
+
except TaskRunsError:
|
|
97
|
+
raise
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error("Unexpected error getting latest task run", error=str(e))
|
|
100
|
+
raise TaskRunsError(f"Unexpected error: {str(e)}") from e
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Tool for running manual tasks on a connected mobile device."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from fastmcp.exceptions import ToolError
|
|
8
|
+
from fastmcp.tools.tool import ToolResult
|
|
9
|
+
from mcp.types import TextContent
|
|
7
10
|
from minitap.mobile_use.sdk.types import ManualTaskConfig
|
|
8
11
|
from minitap.mobile_use.sdk.types.task import PlatformTaskRequest
|
|
9
12
|
from pydantic import Field
|
|
@@ -11,10 +14,15 @@ from pydantic import Field
|
|
|
11
14
|
from minitap.mcp.core.cloud_apk import install_apk_on_cloud_mobile, upload_apk_to_cloud_mobile
|
|
12
15
|
from minitap.mcp.core.config import settings
|
|
13
16
|
from minitap.mcp.core.decorators import handle_tool_errors
|
|
17
|
+
from minitap.mcp.core.logging_config import get_logger
|
|
14
18
|
from minitap.mcp.core.sdk_agent import get_mobile_use_agent
|
|
19
|
+
from minitap.mcp.core.storage import StorageDownloadError, download_trajectory_gif
|
|
20
|
+
from minitap.mcp.core.task_runs import TaskRunsError, get_latest_task_run_id
|
|
15
21
|
from minitap.mcp.main import mcp
|
|
16
22
|
from minitap.mcp.server.cloud_mobile import check_cloud_mobile_status
|
|
17
23
|
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
18
26
|
|
|
19
27
|
def _serialize_result(result: Any) -> Any:
|
|
20
28
|
"""Convert SDK responses to serializable data for MCP."""
|
|
@@ -77,7 +85,7 @@ async def execute_mobile_command(
|
|
|
77
85
|
"The APK will be uploaded to cloud storage and installed before task execution. "
|
|
78
86
|
"Requires MINITAP_API_KEY to be configured. ",
|
|
79
87
|
),
|
|
80
|
-
) -> str | dict[str, Any]:
|
|
88
|
+
) -> str | dict[str, Any] | ToolResult:
|
|
81
89
|
"""Run a manual task on a mobile device via the Minitap platform."""
|
|
82
90
|
try:
|
|
83
91
|
if settings.CLOUD_MOBILE_NAME:
|
|
@@ -110,6 +118,65 @@ async def execute_mobile_command(
|
|
|
110
118
|
request=request,
|
|
111
119
|
locked_app_package=locked_app_package,
|
|
112
120
|
)
|
|
113
|
-
|
|
121
|
+
|
|
122
|
+
trajectory_gif_path: Path | None = None
|
|
123
|
+
if settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER:
|
|
124
|
+
trajectory_gif_path = await _download_trajectory_gif_if_available()
|
|
125
|
+
|
|
126
|
+
serialized_result = _serialize_result(result)
|
|
127
|
+
|
|
128
|
+
# If trajectory was saved, return a ToolResult with multiple content items
|
|
129
|
+
if trajectory_gif_path:
|
|
130
|
+
import json
|
|
131
|
+
|
|
132
|
+
result_text = (
|
|
133
|
+
json.dumps(serialized_result, indent=2)
|
|
134
|
+
if isinstance(serialized_result, dict)
|
|
135
|
+
else str(serialized_result)
|
|
136
|
+
)
|
|
137
|
+
return ToolResult(
|
|
138
|
+
content=[
|
|
139
|
+
TextContent(type="text", text=result_text),
|
|
140
|
+
TextContent(type="text", text=f"Trajectory saved to {trajectory_gif_path}"),
|
|
141
|
+
],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return serialized_result
|
|
114
145
|
except Exception as e:
|
|
115
146
|
raise ToolError(str(e))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def _download_trajectory_gif_if_available() -> Path | None:
|
|
150
|
+
"""Download the trajectory GIF if available and folder is configured.
|
|
151
|
+
|
|
152
|
+
Fetches the latest task run ID from the API and downloads the GIF.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The path to the downloaded GIF file, or None if download failed or not configured.
|
|
156
|
+
"""
|
|
157
|
+
download_folder = settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER
|
|
158
|
+
if not download_folder:
|
|
159
|
+
logger.warning("TRAJECTORY_GIF_DOWNLOAD_FOLDER not configured, skipping GIF download")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
task_run_id = None
|
|
163
|
+
try:
|
|
164
|
+
task_run_id = await get_latest_task_run_id()
|
|
165
|
+
|
|
166
|
+
gif_path = await download_trajectory_gif(
|
|
167
|
+
task_run_id=task_run_id,
|
|
168
|
+
download_path=download_folder,
|
|
169
|
+
)
|
|
170
|
+
logger.info(
|
|
171
|
+
"Trajectory GIF downloaded",
|
|
172
|
+
task_run_id=task_run_id,
|
|
173
|
+
path=str(gif_path),
|
|
174
|
+
)
|
|
175
|
+
return gif_path
|
|
176
|
+
except (StorageDownloadError, TaskRunsError) as e:
|
|
177
|
+
logger.warning(
|
|
178
|
+
"Failed to download trajectory GIF",
|
|
179
|
+
task_run_id=task_run_id,
|
|
180
|
+
error=str(e),
|
|
181
|
+
)
|
|
182
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "minitap-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "Model Context Protocol server for controlling Android & iOS devices with natural language"
|
|
5
5
|
readme = "PYPI_README.md"
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
"python-dotenv>=1.1.1",
|
|
18
18
|
"pydantic>=2.12.0",
|
|
19
19
|
"pydantic-settings>=2.10.1",
|
|
20
|
-
"minitap-mobile-use>=3.
|
|
20
|
+
"minitap-mobile-use>=3.4.0",
|
|
21
21
|
"jinja2>=3.1.6",
|
|
22
22
|
"langchain-core>=0.3.75",
|
|
23
23
|
"pillow>=11.1.0",
|
|
File without changes
|
|
File without changes
|
{minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{minitap_mcp-0.8.0 → minitap_mcp-0.9.0}/minitap/mcp/core/agents/compare_screenshots/prompt.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|