minitap-mcp 0.5.2__tar.gz → 0.6.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.5.2 → minitap_mcp-0.6.0}/PKG-INFO +57 -4
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/PYPI_README.md +54 -2
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/agents/compare_screenshots/agent.py +12 -2
- minitap_mcp-0.6.0/minitap/mcp/core/cloud_apk.py +109 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/config.py +7 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/sdk_agent.py +8 -0
- minitap_mcp-0.6.0/minitap/mcp/main.py +276 -0
- minitap_mcp-0.6.0/minitap/mcp/server/cloud_mobile.py +397 -0
- minitap_mcp-0.6.0/minitap/mcp/server/middleware.py +21 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/server/poller.py +6 -6
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/tools/analyze_screen.py +14 -3
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/tools/compare_screenshot_with_figma.py +1 -1
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/tools/execute_mobile_command.py +47 -8
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/pyproject.toml +3 -2
- minitap_mcp-0.5.2/minitap/mcp/main.py +0 -150
- minitap_mcp-0.5.2/minitap/mcp/server/middleware.py +0 -23
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/__init__.py +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/agents/compare_screenshots/eval/prompts/prompt_1.md +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/actual.png +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/figma.png +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/human_feedback.txt +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.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.5.2 → minitap_mcp-0.6.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.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/agents/compare_screenshots/prompt.md +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/decorators.py +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/device.py +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/llm.py +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/logging_config.py +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/models.py +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/utils/figma.py +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/utils/images.py +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/tools/save_figma_assets.py +0 -0
- {minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/tools/screen_analyzer.md +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: minitap-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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>=
|
|
10
|
+
Requires-Dist: minitap-mobile-use>=3.0.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
|
|
15
16
|
Requires-Dist: ruff==0.5.3 ; extra == 'dev'
|
|
16
17
|
Requires-Dist: pytest==8.4.1 ; extra == 'dev'
|
|
17
18
|
Requires-Dist: pytest-cov==5.0.0 ; extra == 'dev'
|
|
@@ -40,11 +41,19 @@ Before running the MCP server, ensure you have the required mobile automation to
|
|
|
40
41
|
- **For Android devices:**
|
|
41
42
|
|
|
42
43
|
- [ADB (Android Debug Bridge)](https://developer.android.com/tools/adb) - For device communication
|
|
43
|
-
- [Maestro](https://maestro.mobile.dev/) - For mobile automation
|
|
44
44
|
|
|
45
45
|
- **For iOS devices (macOS only):**
|
|
46
46
|
- Xcode Command Line Tools with `xcrun`
|
|
47
|
-
- [
|
|
47
|
+
- **[fb-idb](https://fbidb.io/docs/installation/)**: Facebook's iOS Development Bridge for device automation.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Install via Homebrew (macOS)
|
|
51
|
+
brew tap facebook/fb
|
|
52
|
+
brew install idb-companion
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> [!NOTE]
|
|
56
|
+
> `idb_companion` is required to communicate with iOS simulators. Make sure it's in your PATH after installation.
|
|
48
57
|
|
|
49
58
|
For detailed setup instructions, see the [mobile-use repository](https://github.com/minitap-ai/mobile-use).
|
|
50
59
|
|
|
@@ -244,6 +253,50 @@ The tool will:
|
|
|
244
253
|
3. Compare both screenshots using vision AI
|
|
245
254
|
4. Return a detailed analysis highlighting differences
|
|
246
255
|
|
|
256
|
+
## Cloud Mobile Mode
|
|
257
|
+
|
|
258
|
+
Run the MCP server with cloud-hosted mobile devices instead of requiring a local device. This enables:
|
|
259
|
+
|
|
260
|
+
- **Zero local setup**: No ADB or physical device required
|
|
261
|
+
- **Remote development**: Control cloud mobiles from anywhere
|
|
262
|
+
- **Scalable automation**: Access multiple cloud devices
|
|
263
|
+
|
|
264
|
+
### Setting Up Cloud Mobile Mode
|
|
265
|
+
|
|
266
|
+
1. **Create a Cloud Mobile** on [Minitap Platform](https://platform.minitap.ai/cloud-mobiles):
|
|
267
|
+
- Click **Create New Device**
|
|
268
|
+
- Choose platform (currently Android v11 / API level 30)
|
|
269
|
+
- Set a **Reference Name** (e.g., `my-dev-device`)
|
|
270
|
+
|
|
271
|
+
2. **Configure the environment variable**:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# Using reference name (recommended)
|
|
275
|
+
export CLOUD_MOBILE_NAME="my-dev-device"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
3. **Start the server** (no local device needed):
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
minitap-mcp --server --api-key your_minitap_api_key
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The server will:
|
|
285
|
+
- Connect to your cloud mobile on startup
|
|
286
|
+
- Maintain a keep-alive connection while running
|
|
287
|
+
- Automatically disconnect when the server stops
|
|
288
|
+
|
|
289
|
+
> ⚠️ **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.
|
|
290
|
+
|
|
291
|
+
### Cloud vs Local Mode
|
|
292
|
+
|
|
293
|
+
| Feature | Local Mode | Cloud Mode |
|
|
294
|
+
|---------|------------|------------|
|
|
295
|
+
| Device requirement | Physical/emulator | None |
|
|
296
|
+
| Setup complexity | ADB setup required | Low (env var only) |
|
|
297
|
+
| `CLOUD_MOBILE_NAME` | Not set | Set to device name/UUID |
|
|
298
|
+
| Billing | None | Per-minute usage |
|
|
299
|
+
|
|
247
300
|
## Advanced Configuration
|
|
248
301
|
|
|
249
302
|
### Custom ADB Server
|
|
@@ -17,11 +17,19 @@ Before running the MCP server, ensure you have the required mobile automation to
|
|
|
17
17
|
- **For Android devices:**
|
|
18
18
|
|
|
19
19
|
- [ADB (Android Debug Bridge)](https://developer.android.com/tools/adb) - For device communication
|
|
20
|
-
- [Maestro](https://maestro.mobile.dev/) - For mobile automation
|
|
21
20
|
|
|
22
21
|
- **For iOS devices (macOS only):**
|
|
23
22
|
- Xcode Command Line Tools with `xcrun`
|
|
24
|
-
- [
|
|
23
|
+
- **[fb-idb](https://fbidb.io/docs/installation/)**: Facebook's iOS Development Bridge for device automation.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Install via Homebrew (macOS)
|
|
27
|
+
brew tap facebook/fb
|
|
28
|
+
brew install idb-companion
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> [!NOTE]
|
|
32
|
+
> `idb_companion` is required to communicate with iOS simulators. Make sure it's in your PATH after installation.
|
|
25
33
|
|
|
26
34
|
For detailed setup instructions, see the [mobile-use repository](https://github.com/minitap-ai/mobile-use).
|
|
27
35
|
|
|
@@ -221,6 +229,50 @@ The tool will:
|
|
|
221
229
|
3. Compare both screenshots using vision AI
|
|
222
230
|
4. Return a detailed analysis highlighting differences
|
|
223
231
|
|
|
232
|
+
## Cloud Mobile Mode
|
|
233
|
+
|
|
234
|
+
Run the MCP server with cloud-hosted mobile devices instead of requiring a local device. This enables:
|
|
235
|
+
|
|
236
|
+
- **Zero local setup**: No ADB or physical device required
|
|
237
|
+
- **Remote development**: Control cloud mobiles from anywhere
|
|
238
|
+
- **Scalable automation**: Access multiple cloud devices
|
|
239
|
+
|
|
240
|
+
### Setting Up Cloud Mobile Mode
|
|
241
|
+
|
|
242
|
+
1. **Create a Cloud Mobile** on [Minitap Platform](https://platform.minitap.ai/cloud-mobiles):
|
|
243
|
+
- Click **Create New Device**
|
|
244
|
+
- Choose platform (currently Android v11 / API level 30)
|
|
245
|
+
- Set a **Reference Name** (e.g., `my-dev-device`)
|
|
246
|
+
|
|
247
|
+
2. **Configure the environment variable**:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# Using reference name (recommended)
|
|
251
|
+
export CLOUD_MOBILE_NAME="my-dev-device"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
3. **Start the server** (no local device needed):
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
minitap-mcp --server --api-key your_minitap_api_key
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The server will:
|
|
261
|
+
- Connect to your cloud mobile on startup
|
|
262
|
+
- Maintain a keep-alive connection while running
|
|
263
|
+
- Automatically disconnect when the server stops
|
|
264
|
+
|
|
265
|
+
> ⚠️ **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.
|
|
266
|
+
|
|
267
|
+
### Cloud vs Local Mode
|
|
268
|
+
|
|
269
|
+
| Feature | Local Mode | Cloud Mode |
|
|
270
|
+
|---------|------------|------------|
|
|
271
|
+
| Device requirement | Physical/emulator | None |
|
|
272
|
+
| Setup complexity | ADB setup required | Low (env var only) |
|
|
273
|
+
| `CLOUD_MOBILE_NAME` | Not set | Set to device name/UUID |
|
|
274
|
+
| Billing | None | Per-minute usage |
|
|
275
|
+
|
|
224
276
|
## Advanced Configuration
|
|
225
277
|
|
|
226
278
|
### Custom ADB Server
|
{minitap_mcp-0.5.2 → minitap_mcp-0.6.0}/minitap/mcp/core/agents/compare_screenshots/agent.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import base64
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from uuid import uuid4
|
|
4
5
|
|
|
@@ -9,6 +10,7 @@ from pydantic import BaseModel
|
|
|
9
10
|
from minitap.mcp.core.device import capture_screenshot, find_mobile_device
|
|
10
11
|
from minitap.mcp.core.llm import get_minitap_llm
|
|
11
12
|
from minitap.mcp.core.utils.images import get_screenshot_message_for_llm
|
|
13
|
+
from minitap.mcp.server.cloud_mobile import get_cloud_mobile_id, get_cloud_screenshot
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class CompareScreenshotsOutput(BaseModel):
|
|
@@ -23,6 +25,8 @@ async def compare_screenshots(
|
|
|
23
25
|
"""
|
|
24
26
|
Compare screenshots and return the comparison text along with both screenshots.
|
|
25
27
|
|
|
28
|
+
Supports both local devices (Android/iOS) and cloud devices.
|
|
29
|
+
|
|
26
30
|
Returns:
|
|
27
31
|
CompareScreenshotsOutput
|
|
28
32
|
"""
|
|
@@ -30,8 +34,14 @@ async def compare_screenshots(
|
|
|
30
34
|
Path(__file__).parent.joinpath("prompt.md").read_text(encoding="utf-8")
|
|
31
35
|
).render()
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
cloud_mobile_id = get_cloud_mobile_id()
|
|
38
|
+
|
|
39
|
+
if cloud_mobile_id:
|
|
40
|
+
screenshot_bytes = await get_cloud_screenshot(cloud_mobile_id)
|
|
41
|
+
current_screenshot = base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
42
|
+
else:
|
|
43
|
+
device = find_mobile_device()
|
|
44
|
+
current_screenshot = capture_screenshot(device)
|
|
35
45
|
|
|
36
46
|
messages: list[BaseMessage] = [
|
|
37
47
|
SystemMessage(content=system_message),
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Cloud APK deployment utilities for uploading and installing APKs on cloud mobiles."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from minitap.mcp.core.config import settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def upload_apk_to_cloud_mobile(apk_path: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Upload an APK file via Platform storage API to user storage bucket.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
apk_path: Path to the APK file
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Filename to use with install-apk endpoint
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
FileNotFoundError: If APK file doesn't exist
|
|
23
|
+
httpx.HTTPError: If upload fails
|
|
24
|
+
ValueError: If MINITAP_API_KEY or MINITAP_API_BASE_URL is not configured
|
|
25
|
+
"""
|
|
26
|
+
if not settings.MINITAP_API_KEY:
|
|
27
|
+
raise ValueError("MINITAP_API_KEY is not configured")
|
|
28
|
+
if not settings.MINITAP_API_BASE_URL:
|
|
29
|
+
raise ValueError("MINITAP_API_BASE_URL is not configured")
|
|
30
|
+
|
|
31
|
+
apk_file = Path(apk_path)
|
|
32
|
+
if not apk_file.exists():
|
|
33
|
+
raise FileNotFoundError(f"APK file not found: {apk_path}")
|
|
34
|
+
|
|
35
|
+
filename = f"app_{uuid.uuid4().hex[:6]}.apk"
|
|
36
|
+
api_key = settings.MINITAP_API_KEY.get_secret_value()
|
|
37
|
+
api_base_url = settings.MINITAP_API_BASE_URL.rstrip("/")
|
|
38
|
+
|
|
39
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
40
|
+
# Step 1: Get signed upload URL from storage API
|
|
41
|
+
response = await client.get(
|
|
42
|
+
f"{api_base_url}/storage/signed-upload",
|
|
43
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
44
|
+
params={"filenames": filename},
|
|
45
|
+
)
|
|
46
|
+
response.raise_for_status()
|
|
47
|
+
upload_data = response.json()
|
|
48
|
+
|
|
49
|
+
# Extract the signed URL for our file
|
|
50
|
+
signed_urls = upload_data.get("signed_urls", {})
|
|
51
|
+
if filename not in signed_urls:
|
|
52
|
+
raise ValueError(f"No signed URL returned for {filename}")
|
|
53
|
+
|
|
54
|
+
signed_url = signed_urls[filename]
|
|
55
|
+
|
|
56
|
+
# Step 2: Upload APK to signed URL
|
|
57
|
+
with open(apk_file, "rb") as f:
|
|
58
|
+
upload_response = await client.put(
|
|
59
|
+
signed_url,
|
|
60
|
+
content=f.read(),
|
|
61
|
+
headers={"Content-Type": "application/vnd.android.package-archive"},
|
|
62
|
+
)
|
|
63
|
+
upload_response.raise_for_status()
|
|
64
|
+
|
|
65
|
+
# Step 3: Return filename for install-apk call
|
|
66
|
+
return filename
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def install_apk_on_cloud_mobile(filename: str) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Install an APK on a cloud mobile device via mobile-manager API.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
filename: Filename returned from upload_apk_to_cloud_mobile
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
httpx.HTTPError: If installation fails
|
|
78
|
+
ValueError: If required config settings are not configured
|
|
79
|
+
"""
|
|
80
|
+
if not settings.MINITAP_API_KEY:
|
|
81
|
+
raise ValueError("MINITAP_API_KEY is not configured")
|
|
82
|
+
if not settings.MINITAP_DAAS_API:
|
|
83
|
+
raise ValueError("MINITAP_DAAS_API is not configured")
|
|
84
|
+
if not settings.CLOUD_MOBILE_NAME:
|
|
85
|
+
raise ValueError("CLOUD_MOBILE_NAME is not configured")
|
|
86
|
+
|
|
87
|
+
api_key = settings.MINITAP_API_KEY.get_secret_value()
|
|
88
|
+
base_url = settings.MINITAP_DAAS_API
|
|
89
|
+
cloud_mobile_name = settings.CLOUD_MOBILE_NAME
|
|
90
|
+
|
|
91
|
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
92
|
+
cloud_mobile_response = await client.get(
|
|
93
|
+
f"{base_url}/virtual-mobiles/{cloud_mobile_name}",
|
|
94
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
95
|
+
)
|
|
96
|
+
cloud_mobile_response.raise_for_status()
|
|
97
|
+
response_data = cloud_mobile_response.json()
|
|
98
|
+
cloud_mobile_uuid = response_data.get("id")
|
|
99
|
+
if not cloud_mobile_uuid:
|
|
100
|
+
raise ValueError(f"Cloud mobile '{cloud_mobile_name}' response missing 'id' field")
|
|
101
|
+
response = await client.post(
|
|
102
|
+
f"{base_url}/virtual-mobiles/{cloud_mobile_uuid}/install-apk",
|
|
103
|
+
headers={
|
|
104
|
+
"Authorization": f"Bearer {api_key}",
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
},
|
|
107
|
+
json={"filename": filename},
|
|
108
|
+
)
|
|
109
|
+
response.raise_for_status()
|
|
@@ -16,6 +16,7 @@ class MCPSettings(BaseSettings):
|
|
|
16
16
|
# Minitap API configuration
|
|
17
17
|
MINITAP_API_KEY: SecretStr | None = Field(default=None)
|
|
18
18
|
MINITAP_API_BASE_URL: str = Field(default="https://platform.minitap.ai/api/v1")
|
|
19
|
+
MINITAP_DAAS_API: str = Field(default="https://platform.minitap.ai/api/daas")
|
|
19
20
|
OPEN_ROUTER_API_KEY: SecretStr | None = Field(default=None)
|
|
20
21
|
|
|
21
22
|
VISION_MODEL: str = Field(default="qwen/qwen-2.5-vl-7b-instruct")
|
|
@@ -27,5 +28,11 @@ class MCPSettings(BaseSettings):
|
|
|
27
28
|
MCP_SERVER_HOST: str = Field(default="0.0.0.0")
|
|
28
29
|
MCP_SERVER_PORT: int = Field(default=8000)
|
|
29
30
|
|
|
31
|
+
# Cloud Mobile configuration
|
|
32
|
+
# When set, the MCP server runs in cloud mode connecting to a Minitap cloud mobile
|
|
33
|
+
# instead of requiring a local device. Value can be a device name.
|
|
34
|
+
# Create cloud mobiles at https://platform.minitap.ai/cloud-mobiles
|
|
35
|
+
CLOUD_MOBILE_NAME: str | None = Field(default=None)
|
|
36
|
+
|
|
30
37
|
|
|
31
38
|
settings = MCPSettings() # type: ignore
|
|
@@ -3,6 +3,8 @@ import os
|
|
|
3
3
|
from minitap.mobile_use.sdk import Agent
|
|
4
4
|
from minitap.mobile_use.sdk.builders import Builders
|
|
5
5
|
|
|
6
|
+
from minitap.mcp.core.config import settings
|
|
7
|
+
|
|
6
8
|
# Lazy-initialized singleton agent
|
|
7
9
|
_agent: Agent | None = None
|
|
8
10
|
|
|
@@ -23,5 +25,11 @@ def get_mobile_use_agent() -> Agent:
|
|
|
23
25
|
raise ValueError(f"Invalid ADB server socket: {custom_adb_socket}")
|
|
24
26
|
_, host, port = parts
|
|
25
27
|
config = config.with_adb_server(host=host, port=int(port))
|
|
28
|
+
|
|
29
|
+
# Add cloud mobile configuration if set
|
|
30
|
+
if settings.CLOUD_MOBILE_NAME:
|
|
31
|
+
config = config.for_cloud_mobile(cloud_mobile_id_or_ref=settings.CLOUD_MOBILE_NAME)
|
|
32
|
+
|
|
26
33
|
_agent = Agent(config=config.build())
|
|
34
|
+
|
|
27
35
|
return _agent
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""MCP server for mobile-use with screen analysis capabilities."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
# Fix Windows console encoding for Unicode characters (emojis in logs)
|
|
12
|
+
if sys.platform == "win32":
|
|
13
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
14
|
+
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
|
|
15
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
16
|
+
sys.stderr.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
|
|
17
|
+
os.environ["PYTHONIOENCODING"] = "utf-8"
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import colorama
|
|
21
|
+
|
|
22
|
+
colorama.init(strip=False, convert=True, wrap=True)
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
from fastmcp import FastMCP # noqa: E402
|
|
28
|
+
from minitap.mobile_use.config import settings as sdk_settings
|
|
29
|
+
|
|
30
|
+
from minitap.mcp.core.config import settings # noqa: E402
|
|
31
|
+
from minitap.mcp.core.device import (
|
|
32
|
+
DeviceInfo, # noqa: E402
|
|
33
|
+
list_available_devices,
|
|
34
|
+
)
|
|
35
|
+
from minitap.mcp.core.logging_config import (
|
|
36
|
+
configure_logging, # noqa: E402
|
|
37
|
+
get_logger,
|
|
38
|
+
)
|
|
39
|
+
from minitap.mcp.server.cloud_mobile import CloudMobileService
|
|
40
|
+
from minitap.mcp.server.middleware import LocalDeviceHealthMiddleware
|
|
41
|
+
from minitap.mcp.server.poller import device_health_poller
|
|
42
|
+
|
|
43
|
+
configure_logging(log_level=os.getenv("LOG_LEVEL", "INFO"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> None:
|
|
47
|
+
"""Main entry point for the MCP server."""
|
|
48
|
+
|
|
49
|
+
parser = argparse.ArgumentParser(description="Mobile Use MCP Server")
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--api-key",
|
|
52
|
+
type=str,
|
|
53
|
+
required=False,
|
|
54
|
+
default=None,
|
|
55
|
+
help="Minitap API key for authentication",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--cloud-mobile-name",
|
|
59
|
+
type=str,
|
|
60
|
+
required=False,
|
|
61
|
+
default=None,
|
|
62
|
+
help="Name of the cloud mobile device to connect to (enables cloud mode)",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument("--llm-profile", type=str, required=False, default=None)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--server",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Run as network server (uses MCP_SERVER_HOST and MCP_SERVER_PORT from env)",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--port",
|
|
72
|
+
type=int,
|
|
73
|
+
required=False,
|
|
74
|
+
default=None,
|
|
75
|
+
help="Port to run the server on (overrides MCP_SERVER_PORT env variable)",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
args = parser.parse_args()
|
|
79
|
+
|
|
80
|
+
if args.api_key:
|
|
81
|
+
os.environ["MINITAP_API_KEY"] = args.api_key
|
|
82
|
+
settings.__init__()
|
|
83
|
+
sdk_settings.__init__()
|
|
84
|
+
|
|
85
|
+
if args.cloud_mobile_name:
|
|
86
|
+
os.environ["CLOUD_MOBILE_NAME"] = args.cloud_mobile_name
|
|
87
|
+
settings.__init__()
|
|
88
|
+
sdk_settings.__init__()
|
|
89
|
+
|
|
90
|
+
if args.llm_profile:
|
|
91
|
+
os.environ["MINITAP_LLM_PROFILE_NAME"] = args.llm_profile
|
|
92
|
+
settings.__init__()
|
|
93
|
+
sdk_settings.__init__()
|
|
94
|
+
|
|
95
|
+
if args.port:
|
|
96
|
+
os.environ["MCP_SERVER_PORT"] = str(args.port)
|
|
97
|
+
settings.__init__()
|
|
98
|
+
sdk_settings.__init__()
|
|
99
|
+
|
|
100
|
+
if not settings.MINITAP_API_KEY:
|
|
101
|
+
raise ValueError("Minitap API key is required to run the MCP")
|
|
102
|
+
|
|
103
|
+
# Run MCP server with optional host/port for remote access
|
|
104
|
+
if args.server:
|
|
105
|
+
logger.info(f"Starting MCP server on {settings.MCP_SERVER_HOST}:{settings.MCP_SERVER_PORT}")
|
|
106
|
+
run_mcp_server(
|
|
107
|
+
transport="http",
|
|
108
|
+
host=settings.MCP_SERVER_HOST,
|
|
109
|
+
port=settings.MCP_SERVER_PORT,
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
logger.info("Starting MCP server in local mode")
|
|
113
|
+
run_mcp_server()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
logger = get_logger(__name__)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class MCPLifespanContext:
|
|
121
|
+
"""Context for MCP server lifespan.
|
|
122
|
+
|
|
123
|
+
Stores references to services that need cleanup on shutdown.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
cloud_mobile_service: CloudMobileService | None = None
|
|
127
|
+
local_poller_stop_event: threading.Event | None = None
|
|
128
|
+
local_poller_thread: threading.Thread | None = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@asynccontextmanager
|
|
132
|
+
async def mcp_lifespan(server: FastMCP) -> AsyncIterator[MCPLifespanContext]:
|
|
133
|
+
"""Lifespan context manager for MCP server.
|
|
134
|
+
|
|
135
|
+
Handles startup/shutdown for both cloud and local modes.
|
|
136
|
+
|
|
137
|
+
Cloud mode (CLOUD_MOBILE_NAME set):
|
|
138
|
+
- Connects to cloud mobile on startup
|
|
139
|
+
- Maintains keep-alive polling
|
|
140
|
+
- Disconnects on shutdown (critical for billing!)
|
|
141
|
+
|
|
142
|
+
Local mode (CLOUD_MOBILE_NAME not set):
|
|
143
|
+
- Starts device health poller
|
|
144
|
+
- Monitors local device connection
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
server: The FastMCP server instance.
|
|
148
|
+
|
|
149
|
+
Yields:
|
|
150
|
+
MCPLifespanContext with references to running services.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
RuntimeError: If cloud mobile connection fails
|
|
154
|
+
(crashes MCP to prevent false "connected" state).
|
|
155
|
+
"""
|
|
156
|
+
from minitap.mcp.core.sdk_agent import get_mobile_use_agent # noqa: E402
|
|
157
|
+
|
|
158
|
+
context = MCPLifespanContext()
|
|
159
|
+
api_key = settings.MINITAP_API_KEY.get_secret_value() if settings.MINITAP_API_KEY else None
|
|
160
|
+
|
|
161
|
+
# Check if running in cloud mode
|
|
162
|
+
if settings.CLOUD_MOBILE_NAME:
|
|
163
|
+
# ==================== CLOUD MODE ====================
|
|
164
|
+
logger.info(f"Starting MCP in CLOUD mode with mobile: {settings.CLOUD_MOBILE_NAME}")
|
|
165
|
+
|
|
166
|
+
if not api_key:
|
|
167
|
+
logger.error("MINITAP_API_KEY is required")
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
"MINITAP_API_KEY is required when CLOUD_MOBILE_NAME is set. "
|
|
170
|
+
"Please set your API key via --api-key or MINITAP_API_KEY environment variable."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Create and connect cloud mobile service
|
|
174
|
+
cloud_service = CloudMobileService(
|
|
175
|
+
cloud_mobile_name=settings.CLOUD_MOBILE_NAME,
|
|
176
|
+
api_key=api_key,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
await cloud_service.connect()
|
|
181
|
+
except Exception as e:
|
|
182
|
+
# CRITICAL: If cloud mobile not found, crash the MCP!
|
|
183
|
+
# This prevents the IDE from showing the server as "connected"
|
|
184
|
+
# when it actually can't do anything useful.
|
|
185
|
+
logger.error(f"Failed to connect to cloud mobile: {e}")
|
|
186
|
+
raise RuntimeError(
|
|
187
|
+
f"Cloud mobile connection failed. The MCP server cannot start.\n{e}"
|
|
188
|
+
) from e
|
|
189
|
+
|
|
190
|
+
context.cloud_mobile_service = cloud_service
|
|
191
|
+
logger.info("Cloud mobile connected, MCP server ready")
|
|
192
|
+
|
|
193
|
+
else:
|
|
194
|
+
# ==================== LOCAL MODE ====================
|
|
195
|
+
logger.info("Starting MCP in LOCAL mode (no CLOUD_MOBILE_NAME set)")
|
|
196
|
+
|
|
197
|
+
agent = get_mobile_use_agent()
|
|
198
|
+
server.add_middleware(LocalDeviceHealthMiddleware(agent))
|
|
199
|
+
|
|
200
|
+
# Start device health poller in background
|
|
201
|
+
stop_event = threading.Event()
|
|
202
|
+
poller_thread = threading.Thread(
|
|
203
|
+
target=device_health_poller,
|
|
204
|
+
args=(stop_event, agent),
|
|
205
|
+
daemon=True,
|
|
206
|
+
)
|
|
207
|
+
poller_thread.start()
|
|
208
|
+
|
|
209
|
+
context.local_poller_stop_event = stop_event
|
|
210
|
+
context.local_poller_thread = poller_thread
|
|
211
|
+
logger.info("Device health poller started")
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
yield context
|
|
215
|
+
finally:
|
|
216
|
+
# ==================== SHUTDOWN ====================
|
|
217
|
+
logger.info("MCP server shutting down, cleaning up resources...")
|
|
218
|
+
|
|
219
|
+
if context.cloud_mobile_service:
|
|
220
|
+
# CRITICAL: Stop cloud mobile connection to stop billing!
|
|
221
|
+
logger.info("Disconnecting cloud mobile (stopping billing)...")
|
|
222
|
+
try:
|
|
223
|
+
await context.cloud_mobile_service.disconnect()
|
|
224
|
+
logger.info("Cloud mobile disconnected successfully")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Error disconnecting cloud mobile: {e}")
|
|
227
|
+
|
|
228
|
+
if context.local_poller_stop_event and context.local_poller_thread:
|
|
229
|
+
# Stop local device health poller
|
|
230
|
+
logger.info("Stopping device health poller...")
|
|
231
|
+
context.local_poller_stop_event.set()
|
|
232
|
+
context.local_poller_thread.join(timeout=10.0)
|
|
233
|
+
|
|
234
|
+
if context.local_poller_thread.is_alive():
|
|
235
|
+
logger.warning("Device health poller thread did not stop gracefully")
|
|
236
|
+
else:
|
|
237
|
+
logger.info("Device health poller stopped successfully")
|
|
238
|
+
|
|
239
|
+
logger.info("MCP server shutdown complete")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Create MCP server with lifespan handler
|
|
243
|
+
mcp = FastMCP(
|
|
244
|
+
name="mobile-use-mcp",
|
|
245
|
+
instructions="""
|
|
246
|
+
This server provides analysis tools for connected
|
|
247
|
+
mobile devices (iOS or Android).
|
|
248
|
+
Call get_available_devices() to list them.
|
|
249
|
+
""",
|
|
250
|
+
lifespan=mcp_lifespan,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
from minitap.mcp.tools import analyze_screen # noqa: E402, F401
|
|
254
|
+
from minitap.mcp.tools import compare_screenshot_with_figma # noqa: E402, F401
|
|
255
|
+
from minitap.mcp.tools import execute_mobile_command # noqa: E402, F401
|
|
256
|
+
from minitap.mcp.tools import save_figma_assets # noqa: E402, F401
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@mcp.resource("data://devices")
|
|
260
|
+
def get_available_devices() -> list[DeviceInfo]:
|
|
261
|
+
"""Provides a list of connected mobile devices (iOS or Android)."""
|
|
262
|
+
return list_available_devices()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def run_mcp_server(**mcp_run_kwargs):
|
|
266
|
+
"""Run the MCP server with proper exception handling.
|
|
267
|
+
|
|
268
|
+
This wraps mcp.run() with exception handling for clean shutdown.
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
mcp.run(**mcp_run_kwargs)
|
|
272
|
+
except KeyboardInterrupt:
|
|
273
|
+
logger.info("Keyboard interrupt received, shutting down...")
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"Error running MCP server: {e}")
|
|
276
|
+
raise
|