minitap-mcp 0.1.1__py3-none-any.whl → 0.4.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/agents/compare_screenshots.md +62 -0
- minitap/mcp/core/agents/compare_screenshots.py +65 -0
- minitap/mcp/core/agents/extract_figma_assets.md +64 -0
- minitap/mcp/core/agents/extract_figma_assets.py +65 -0
- minitap/mcp/core/config.py +4 -1
- minitap/mcp/core/models.py +59 -0
- minitap/mcp/core/sdk_agent.py +27 -0
- minitap/mcp/main.py +67 -43
- minitap/mcp/server/poller.py +49 -9
- minitap/mcp/tools/compare_screenshot_with_figma.py +132 -0
- minitap/mcp/tools/execute_mobile_command.py +5 -3
- minitap/mcp/tools/save_figma_assets.py +258 -0
- minitap_mcp-0.4.0.dist-info/METADATA +203 -0
- minitap_mcp-0.4.0.dist-info/RECORD +25 -0
- {minitap_mcp-0.1.1.dist-info → minitap_mcp-0.4.0.dist-info}/WHEEL +1 -1
- minitap/mcp/core/agents.py +0 -19
- minitap_mcp-0.1.1.dist-info/METADATA +0 -348
- minitap_mcp-0.1.1.dist-info/RECORD +0 -18
- {minitap_mcp-0.1.1.dist-info → minitap_mcp-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Tool for navigating to a screen and comparing it with Figma design."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
|
|
6
|
+
import mcp as mcp_ref
|
|
7
|
+
from fastmcp import Client
|
|
8
|
+
from fastmcp.client.client import CallToolResult
|
|
9
|
+
from fastmcp.exceptions import ToolError
|
|
10
|
+
from fastmcp.tools.tool import ToolResult
|
|
11
|
+
from PIL import Image
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
from minitap.mcp.core.agents.compare_screenshots import compare_screenshots
|
|
15
|
+
from minitap.mcp.core.config import settings
|
|
16
|
+
from minitap.mcp.core.decorators import handle_tool_errors
|
|
17
|
+
from minitap.mcp.main import mcp
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp.tool(
|
|
21
|
+
name="compare_screenshot_with_figma",
|
|
22
|
+
description="""
|
|
23
|
+
Compare a screenshot of the current state with a Figma design.
|
|
24
|
+
|
|
25
|
+
This tool:
|
|
26
|
+
1. Captures a screenshot of the current state
|
|
27
|
+
2. Compares the live device screenshot with the Figma design
|
|
28
|
+
3. Returns a detailed comparison report with both screenshots for visual context
|
|
29
|
+
""",
|
|
30
|
+
)
|
|
31
|
+
@handle_tool_errors
|
|
32
|
+
async def compare_screenshot_with_figma(
|
|
33
|
+
node_id: str = Field(
|
|
34
|
+
description=(
|
|
35
|
+
"The node ID of the Figma design. Expected format is ':' separated.\n"
|
|
36
|
+
"Example: If given the URL https://figma.com/design/:fileKey/:fileName?node-id=1-2,\n"
|
|
37
|
+
"the extracted nodeId would be 1:2. Strictly respect this format."
|
|
38
|
+
)
|
|
39
|
+
),
|
|
40
|
+
) -> ToolResult:
|
|
41
|
+
expected_screenshot_base64 = await get_figma_screenshot(node_id)
|
|
42
|
+
|
|
43
|
+
result = await compare_screenshots(
|
|
44
|
+
expected_screenshot_base64=expected_screenshot_base64,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
compressed_expected = compress_image_base64(result.expected_screenshot_base64)
|
|
48
|
+
compressed_current = compress_image_base64(result.current_screenshot_base64)
|
|
49
|
+
|
|
50
|
+
return ToolResult(
|
|
51
|
+
content=[
|
|
52
|
+
mcp_ref.types.TextContent(
|
|
53
|
+
type="text",
|
|
54
|
+
text="## Comparison Analysis\n\n" + str(result.comparison_text),
|
|
55
|
+
),
|
|
56
|
+
mcp_ref.types.ImageContent(
|
|
57
|
+
type="image",
|
|
58
|
+
data=compressed_expected,
|
|
59
|
+
mimeType="image/jpeg",
|
|
60
|
+
),
|
|
61
|
+
mcp_ref.types.TextContent(
|
|
62
|
+
type="text",
|
|
63
|
+
text="**Expected (Figma design)** ↑\n\n**Actual (Current device)** ↓",
|
|
64
|
+
),
|
|
65
|
+
mcp_ref.types.ImageContent(
|
|
66
|
+
type="image",
|
|
67
|
+
data=compressed_current,
|
|
68
|
+
mimeType="image/jpeg",
|
|
69
|
+
),
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def compress_image_base64(base64_str: str, max_width: int = 800, quality: int = 75) -> str:
|
|
75
|
+
"""Compress and resize a base64-encoded image to reduce size.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
base64_str: Base64-encoded image string
|
|
79
|
+
max_width: Maximum width for the resized image
|
|
80
|
+
quality: JPEG quality (1-95, lower = smaller file)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Compressed base64-encoded image string
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
img_data = base64.b64decode(base64_str)
|
|
87
|
+
img = Image.open(BytesIO(img_data))
|
|
88
|
+
|
|
89
|
+
if img.mode in ("RGBA", "P", "LA"):
|
|
90
|
+
background = Image.new("RGB", img.size, (255, 255, 255))
|
|
91
|
+
if img.mode == "P":
|
|
92
|
+
img = img.convert("RGBA")
|
|
93
|
+
if "A" in img.mode:
|
|
94
|
+
background.paste(img, mask=img.split()[-1])
|
|
95
|
+
else:
|
|
96
|
+
background.paste(img)
|
|
97
|
+
img = background
|
|
98
|
+
elif img.mode != "RGB":
|
|
99
|
+
img = img.convert("RGB")
|
|
100
|
+
|
|
101
|
+
if img.width > max_width:
|
|
102
|
+
ratio = max_width / img.width
|
|
103
|
+
new_height = int(img.height * ratio)
|
|
104
|
+
img = img.resize((max_width, new_height), Image.Resampling.LANCZOS)
|
|
105
|
+
|
|
106
|
+
buffer = BytesIO()
|
|
107
|
+
img.save(buffer, format="JPEG", quality=quality, optimize=True)
|
|
108
|
+
compressed_data = buffer.getvalue()
|
|
109
|
+
|
|
110
|
+
return base64.b64encode(compressed_data).decode("utf-8")
|
|
111
|
+
except Exception:
|
|
112
|
+
return base64_str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def get_figma_screenshot(node_id: str) -> str:
|
|
116
|
+
try:
|
|
117
|
+
async with Client(settings.FIGMA_MCP_SERVER_URL) as client:
|
|
118
|
+
result: CallToolResult = await client.call_tool(
|
|
119
|
+
"get_screenshot",
|
|
120
|
+
{
|
|
121
|
+
"nodeId": node_id,
|
|
122
|
+
"clientLanguages": "javascript",
|
|
123
|
+
"clientFrameworks": "react",
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
if len(result.content) == 0 or not isinstance(
|
|
127
|
+
result.content[0], mcp_ref.types.ImageContent
|
|
128
|
+
):
|
|
129
|
+
raise ToolError("Failed to fetch screenshot from Figma")
|
|
130
|
+
return result.content[0].data
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise ToolError(f"Failed to fetch screenshot from Figma: {str(e)}")
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from fastmcp.exceptions import ToolError
|
|
6
7
|
from minitap.mobile_use.sdk.types import ManualTaskConfig
|
|
7
8
|
from minitap.mobile_use.sdk.types.task import PlatformTaskRequest
|
|
8
9
|
from pydantic import Field
|
|
9
10
|
|
|
10
|
-
from minitap.mcp.core.agents import agent
|
|
11
11
|
from minitap.mcp.core.decorators import handle_tool_errors
|
|
12
|
+
from minitap.mcp.core.sdk_agent import get_mobile_use_agent
|
|
12
13
|
from minitap.mcp.main import mcp
|
|
13
14
|
|
|
14
15
|
|
|
@@ -58,7 +59,8 @@ async def execute_mobile_command(
|
|
|
58
59
|
request = PlatformTaskRequest(
|
|
59
60
|
task=ManualTaskConfig(goal=goal, output_description=output_description),
|
|
60
61
|
)
|
|
62
|
+
agent = get_mobile_use_agent()
|
|
61
63
|
result = await agent.run_task(request=request)
|
|
62
64
|
return _serialize_result(result)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise ToolError(str(e))
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Tool for fetching and saving Figma assets locally."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import mcp as mcp_ref
|
|
7
|
+
import requests
|
|
8
|
+
from fastmcp import Client
|
|
9
|
+
from fastmcp.client.client import CallToolResult
|
|
10
|
+
from fastmcp.exceptions import ToolError
|
|
11
|
+
from fastmcp.tools.tool import ToolResult
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
from minitap.mcp.core.agents.extract_figma_assets import (
|
|
15
|
+
ExtractedAssets,
|
|
16
|
+
FigmaAsset,
|
|
17
|
+
extract_figma_assets,
|
|
18
|
+
)
|
|
19
|
+
from minitap.mcp.core.config import settings
|
|
20
|
+
from minitap.mcp.core.decorators import handle_tool_errors
|
|
21
|
+
from minitap.mcp.core.models import (
|
|
22
|
+
AssetDownloadResult,
|
|
23
|
+
AssetDownloadSummary,
|
|
24
|
+
DownloadStatus,
|
|
25
|
+
FigmaDesignContextOutput,
|
|
26
|
+
)
|
|
27
|
+
from minitap.mcp.main import mcp
|
|
28
|
+
from minitap.mcp.tools.compare_screenshot_with_figma import (
|
|
29
|
+
compress_image_base64,
|
|
30
|
+
get_figma_screenshot,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mcp.tool(
|
|
35
|
+
name="save_figma_assets",
|
|
36
|
+
description="""
|
|
37
|
+
Fetch Figma design assets/react implementation code and save them locally in the workspace.
|
|
38
|
+
|
|
39
|
+
This tool:
|
|
40
|
+
1. Calls get_design_context from Figma MCP to get the React/TypeScript code
|
|
41
|
+
2. Extracts all asset URLs and code implementation from the code
|
|
42
|
+
3. Downloads each asset to .mobile-use/figma_assets/<node-id>/ folder
|
|
43
|
+
4. Saves the code implementation to .mobile-use/figma_assets/<node-id>/code_implementation.ts
|
|
44
|
+
5. Returns a list of downloaded files
|
|
45
|
+
""",
|
|
46
|
+
)
|
|
47
|
+
@handle_tool_errors
|
|
48
|
+
async def save_figma_assets(
|
|
49
|
+
node_id: str = Field(
|
|
50
|
+
description=(
|
|
51
|
+
"The node ID of the Figma design. Expected format is ':' separated.\n"
|
|
52
|
+
"Example: If given the URL https://figma.com/design/:fileKey/:fileName?node-id=1-2,\n"
|
|
53
|
+
"the extracted nodeId would be 1:2. Strictly respect this format."
|
|
54
|
+
)
|
|
55
|
+
),
|
|
56
|
+
file_key: str = Field(
|
|
57
|
+
description=(
|
|
58
|
+
"The file key of the Figma file.\n"
|
|
59
|
+
"Example: If given the URL https://figma.com/design/abc123/MyFile?node-id=1-2,\n"
|
|
60
|
+
"the extracted fileKey would be 'abc123'."
|
|
61
|
+
)
|
|
62
|
+
),
|
|
63
|
+
workspace_path: str = Field(
|
|
64
|
+
default=".",
|
|
65
|
+
description=(
|
|
66
|
+
"The workspace path where assets should be saved. Defaults to current directory."
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
) -> ToolResult:
|
|
70
|
+
"""Fetch and save Figma assets locally."""
|
|
71
|
+
|
|
72
|
+
# Step 1: Get design context from Figma MCP
|
|
73
|
+
design_context = await get_design_context(node_id, file_key)
|
|
74
|
+
|
|
75
|
+
# Step 2: Extract asset URLs using LLM agent
|
|
76
|
+
extracted_context: ExtractedAssets = await extract_figma_assets(
|
|
77
|
+
design_context.code_implementation
|
|
78
|
+
)
|
|
79
|
+
if not extracted_context.assets:
|
|
80
|
+
raise ToolError("No assets found in the Figma design context.")
|
|
81
|
+
|
|
82
|
+
# Step 3: Create directory structure
|
|
83
|
+
# Convert node_id format (1:2) to folder name (1-2)
|
|
84
|
+
folder_name = node_id.replace(":", "-")
|
|
85
|
+
assets_dir = Path(workspace_path) / ".mobile-use" / "figma_assets" / folder_name
|
|
86
|
+
|
|
87
|
+
# Delete existing directory to remove stale assets
|
|
88
|
+
if assets_dir.exists():
|
|
89
|
+
shutil.rmtree(assets_dir)
|
|
90
|
+
|
|
91
|
+
# Create fresh directory
|
|
92
|
+
assets_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
# Step 4: Download assets with resilient error handling
|
|
95
|
+
download_summary = AssetDownloadSummary()
|
|
96
|
+
|
|
97
|
+
for asset in extracted_context.assets:
|
|
98
|
+
result = download_asset(asset, assets_dir)
|
|
99
|
+
if result.status == DownloadStatus.SUCCESS:
|
|
100
|
+
download_summary.successful.append(result)
|
|
101
|
+
else:
|
|
102
|
+
download_summary.failed.append(result)
|
|
103
|
+
|
|
104
|
+
# Step 4.5: Save code implementation
|
|
105
|
+
code_implementation_file = assets_dir / "code_implementation.ts"
|
|
106
|
+
|
|
107
|
+
commented_code_implementation_guidelines = ""
|
|
108
|
+
if design_context.code_implementation_guidelines:
|
|
109
|
+
commented_code_implementation_guidelines = "\n".join(
|
|
110
|
+
["// " + line for line in design_context.code_implementation_guidelines.split("\n")]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
commented_nodes_guidelines = ""
|
|
114
|
+
if design_context.nodes_guidelines:
|
|
115
|
+
commented_nodes_guidelines = "\n".join(
|
|
116
|
+
["// " + line for line in design_context.nodes_guidelines.split("\n")]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
code_implementation_file.write_text(
|
|
120
|
+
extracted_context.code_implementation
|
|
121
|
+
+ "\n\n"
|
|
122
|
+
+ commented_code_implementation_guidelines
|
|
123
|
+
+ "\n\n"
|
|
124
|
+
+ commented_nodes_guidelines
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Step 5: Generate friendly output message
|
|
128
|
+
result_parts = []
|
|
129
|
+
|
|
130
|
+
if download_summary.successful:
|
|
131
|
+
result_parts.append(
|
|
132
|
+
f"✅ Successfully downloaded {download_summary.success_count()} asset(s) "
|
|
133
|
+
f"to .mobile-use/figma_assets/{folder_name}/:\n"
|
|
134
|
+
)
|
|
135
|
+
for asset_result in download_summary.successful:
|
|
136
|
+
result_parts.append(f" • {asset_result.filename}")
|
|
137
|
+
|
|
138
|
+
if download_summary.failed:
|
|
139
|
+
result_parts.append(
|
|
140
|
+
f"\n\n⚠️ Failed to download {download_summary.failure_count()} asset(s):"
|
|
141
|
+
)
|
|
142
|
+
for asset_result in download_summary.failed:
|
|
143
|
+
error_msg = f": {asset_result.error}" if asset_result.error else ""
|
|
144
|
+
result_parts.append(f" • {asset_result.filename}{error_msg}")
|
|
145
|
+
|
|
146
|
+
if code_implementation_file.exists():
|
|
147
|
+
result_parts.append(
|
|
148
|
+
f"\n\n✅ Successfully saved code implementation to {code_implementation_file.name}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
expected_screenshot = await get_figma_screenshot(node_id)
|
|
152
|
+
compressed_expected = compress_image_base64(expected_screenshot)
|
|
153
|
+
|
|
154
|
+
return ToolResult(
|
|
155
|
+
content=[
|
|
156
|
+
mcp_ref.types.TextContent(
|
|
157
|
+
type="text",
|
|
158
|
+
text="\n".join(result_parts),
|
|
159
|
+
),
|
|
160
|
+
mcp_ref.types.TextContent(
|
|
161
|
+
type="text",
|
|
162
|
+
text="**Expected (Figma design)**",
|
|
163
|
+
),
|
|
164
|
+
mcp_ref.types.ImageContent(
|
|
165
|
+
type="image",
|
|
166
|
+
data=compressed_expected,
|
|
167
|
+
mimeType="image/jpeg",
|
|
168
|
+
),
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def get_design_context(node_id: str, file_key: str) -> FigmaDesignContextOutput:
|
|
174
|
+
"""Fetch design context from Figma MCP server.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
node_id: The Figma node ID in format "1:2"
|
|
178
|
+
file_key: The Figma file key
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The React/TypeScript code as a string
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ToolError: If fetching fails
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
async with Client(settings.FIGMA_MCP_SERVER_URL) as client:
|
|
188
|
+
result: CallToolResult = await client.call_tool(
|
|
189
|
+
"get_design_context",
|
|
190
|
+
{
|
|
191
|
+
"nodeId": node_id,
|
|
192
|
+
"fileKey": file_key,
|
|
193
|
+
"clientLanguages": "typescript",
|
|
194
|
+
"clientFrameworks": "react",
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
code_implementation = ""
|
|
199
|
+
code_implementation_guidelines = None
|
|
200
|
+
nodes_guidelines = None
|
|
201
|
+
|
|
202
|
+
if len(result.content) > 0 and isinstance(result.content[0], mcp_ref.types.TextContent):
|
|
203
|
+
code_implementation = result.content[0].text
|
|
204
|
+
else:
|
|
205
|
+
raise ToolError("Failed to fetch design context from Figma")
|
|
206
|
+
|
|
207
|
+
if len(result.content) > 1:
|
|
208
|
+
if isinstance(result.content[1], mcp_ref.types.TextContent):
|
|
209
|
+
code_implementation_guidelines = result.content[1].text
|
|
210
|
+
if len(result.content) > 2 and isinstance(result.content[2], mcp_ref.types.TextContent):
|
|
211
|
+
nodes_guidelines = result.content[2].text
|
|
212
|
+
|
|
213
|
+
return FigmaDesignContextOutput(
|
|
214
|
+
code_implementation=code_implementation,
|
|
215
|
+
code_implementation_guidelines=code_implementation_guidelines,
|
|
216
|
+
nodes_guidelines=nodes_guidelines,
|
|
217
|
+
)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
raise ToolError(
|
|
220
|
+
f"Failed to fetch design context from Figma: {str(e)}.\n"
|
|
221
|
+
"Ensure the Figma MCP server is running through the official Figma desktop app."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def download_asset(asset: FigmaAsset, assets_dir: Path) -> AssetDownloadResult:
|
|
226
|
+
"""Download a single asset with error handling.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
asset: FigmaAsset model with variable_name, url, and extension
|
|
230
|
+
assets_dir: Directory to save the asset
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
AssetDownloadResult with status and optional error message
|
|
234
|
+
"""
|
|
235
|
+
variable_name = asset.variable_name
|
|
236
|
+
url = asset.url
|
|
237
|
+
extension = asset.extension
|
|
238
|
+
|
|
239
|
+
# Convert camelCase variable name to filename
|
|
240
|
+
# e.g., imgSignal -> imgSignal.svg
|
|
241
|
+
filename = f"{variable_name}.{extension}"
|
|
242
|
+
filepath = assets_dir / filename
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
response = requests.get(url, timeout=30)
|
|
246
|
+
if response.status_code == 200:
|
|
247
|
+
filepath.write_bytes(response.content)
|
|
248
|
+
return AssetDownloadResult(filename=filename, status=DownloadStatus.SUCCESS)
|
|
249
|
+
else:
|
|
250
|
+
return AssetDownloadResult(
|
|
251
|
+
filename=filename,
|
|
252
|
+
status=DownloadStatus.FAILED,
|
|
253
|
+
error=f"HTTP {response.status_code}",
|
|
254
|
+
)
|
|
255
|
+
except requests.exceptions.Timeout:
|
|
256
|
+
return AssetDownloadResult(filename=filename, status=DownloadStatus.FAILED, error="Timeout")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
return AssetDownloadResult(filename=filename, status=DownloadStatus.FAILED, error=str(e))
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: minitap-mcp
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Model Context Protocol server for controlling Android & iOS devices with natural language
|
|
5
|
+
Author: Pierre-Louis Favreau, Jean-Pierre Lo, Clément Guiguet
|
|
6
|
+
Requires-Dist: fastmcp>=2.12.4
|
|
7
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
8
|
+
Requires-Dist: pydantic>=2.12.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.10.1
|
|
10
|
+
Requires-Dist: minitap-mobile-use>=2.8.1
|
|
11
|
+
Requires-Dist: jinja2>=3.1.6
|
|
12
|
+
Requires-Dist: langchain-core>=0.3.75
|
|
13
|
+
Requires-Dist: pillow>=11.1.0
|
|
14
|
+
Requires-Dist: ruff==0.5.3 ; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest==8.4.1 ; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-cov==5.0.0 ; extra == 'dev'
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Project-URL: Homepage, https://minitap.ai/
|
|
19
|
+
Project-URL: Source, https://github.com/minitap-ai/mobile-use
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Minitap MCP Server
|
|
24
|
+
|
|
25
|
+
A Model Context Protocol (MCP) server that enables AI assistants to control and interact with real mobile devices (Android & iOS) through natural language commands.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install minitap-mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
|
|
37
|
+
Before running the MCP server, ensure you have the required mobile automation tools installed:
|
|
38
|
+
|
|
39
|
+
- **For Android devices:**
|
|
40
|
+
- [ADB (Android Debug Bridge)](https://developer.android.com/tools/adb) - For device communication
|
|
41
|
+
- [Maestro](https://maestro.mobile.dev/) - For mobile automation
|
|
42
|
+
|
|
43
|
+
- **For iOS devices (macOS only):**
|
|
44
|
+
- Xcode Command Line Tools with `xcrun`
|
|
45
|
+
- [Maestro](https://maestro.mobile.dev/) - For mobile automation
|
|
46
|
+
|
|
47
|
+
For detailed setup instructions, see the [mobile-use repository](https://github.com/minitap-ai/mobile-use).
|
|
48
|
+
|
|
49
|
+
### Running the Server
|
|
50
|
+
|
|
51
|
+
The simplest way to start:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
minitap-mcp --server --api-key your_minitap_api_key
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This starts the server on `localhost:8000` with your API key. Get your free API key at [platform.minitap.ai/api-keys](https://platform.minitap.ai/api-keys).
|
|
58
|
+
|
|
59
|
+
**Available CLI options:**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
minitap-mcp --server --api-key YOUR_KEY --llm-profile PROFILE_NAME
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `--api-key`: Your Minitap API key (overrides `MINITAP_API_KEY` env var). Get yours at [platform.minitap.ai/api-keys](https://platform.minitap.ai/api-keys).
|
|
66
|
+
- `--llm-profile`: LLM profile name to use (overrides `MINITAP_LLM_PROFILE_NAME` env var). If unset, uses the default profile. Configure profiles at [platform.minitap.ai/llm-profiles](https://platform.minitap.ai/llm-profiles).
|
|
67
|
+
|
|
68
|
+
### Configuration (Optional)
|
|
69
|
+
|
|
70
|
+
Alternatively, you can set environment variables instead of using CLI flags:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export MINITAP_API_KEY="your_minitap_api_key"
|
|
74
|
+
export MINITAP_API_BASE_URL="https://platform.minitap.ai/api/v1"
|
|
75
|
+
export MINITAP_LLM_PROFILE_NAME="default"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
You can set these in your `.bashrc` or equivalent, then simply run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
minitap-mcp --server
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
CLI flags always override environment variables when both are present.
|
|
85
|
+
|
|
86
|
+
By default, the server will bind to `0.0.0.0:8000`. Configure via environment variables:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
export MCP_SERVER_HOST="0.0.0.0"
|
|
90
|
+
export MCP_SERVER_PORT="8000"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## IDE Integration
|
|
94
|
+
|
|
95
|
+
1. Start the server: `minitap-mcp --server --api-key your_minitap_api_key`
|
|
96
|
+
2. Add to your IDE MCP settings file:
|
|
97
|
+
|
|
98
|
+
```jsonc
|
|
99
|
+
# For Windsurf
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"minitap-mcp": {
|
|
103
|
+
"serverUrl": "http://localhost:8000/mcp"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```jsonc
|
|
110
|
+
# For Cursor
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"minitap-mcp": {
|
|
114
|
+
"transport": "http",
|
|
115
|
+
"url": "http://localhost:8000/mcp"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## Available Tools
|
|
123
|
+
|
|
124
|
+
Once connected, your AI assistant can use these tools:
|
|
125
|
+
|
|
126
|
+
### `execute_mobile_command`
|
|
127
|
+
Execute natural language commands on your mobile device using the Minitap SDK. This tool allows you to control your Android or iOS device using natural language.
|
|
128
|
+
|
|
129
|
+
**Parameters:**
|
|
130
|
+
- `goal` (required): High-level goal describing the action to perform
|
|
131
|
+
- `output_description` (optional): Natural language description of the desired output format. Results are returned as structured JSON (e.g., "An array with sender and subject for each email")
|
|
132
|
+
- `profile` (optional): Profile name to use (defaults to "default")
|
|
133
|
+
|
|
134
|
+
**Examples:**
|
|
135
|
+
```
|
|
136
|
+
"Open the settings app and tell me the battery level"
|
|
137
|
+
"Find the first 3 unread emails in Gmail"
|
|
138
|
+
"Open Google Maps and search for the nearest coffee shop"
|
|
139
|
+
"Take a screenshot and save it"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `analyze_screen`
|
|
143
|
+
Capture and analyze what's currently shown on the mobile device screen using a vision-capable LLM. Useful for understanding UI elements, extracting text, or identifying specific features.
|
|
144
|
+
|
|
145
|
+
**Parameters:**
|
|
146
|
+
- `prompt` (required): Analysis prompt describing what information to extract
|
|
147
|
+
- `device_id` (optional): Specific device ID to target
|
|
148
|
+
|
|
149
|
+
**Examples:**
|
|
150
|
+
```js
|
|
151
|
+
"What app is currently open?"
|
|
152
|
+
"Read the text messages visible on screen"
|
|
153
|
+
"List all buttons and their labels on the current screen"
|
|
154
|
+
"Extract the phone number displayed"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Advanced Configuration
|
|
158
|
+
|
|
159
|
+
### Custom ADB Server
|
|
160
|
+
|
|
161
|
+
If using a remote or custom ADB server (like on WSL):
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
export ADB_SERVER_SOCKET="tcp:192.168.1.100:5037"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Vision Model
|
|
168
|
+
|
|
169
|
+
Customize the vision model used for screen analysis:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
export VISION_MODEL="qwen/qwen-2.5-vl-7b-instruct"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Device Setup
|
|
176
|
+
|
|
177
|
+
### Android
|
|
178
|
+
1. Enable USB debugging on your device
|
|
179
|
+
2. Connect via USB or network ADB
|
|
180
|
+
3. Verify connection: `adb devices`
|
|
181
|
+
|
|
182
|
+
### iOS (macOS only)
|
|
183
|
+
1. Install Xcode Command Line Tools
|
|
184
|
+
2. Start a simulator or connect a physical device
|
|
185
|
+
3. Verify: `xcrun simctl list devices booted`
|
|
186
|
+
|
|
187
|
+
## Troubleshooting
|
|
188
|
+
|
|
189
|
+
**No devices found:**
|
|
190
|
+
- Verify ADB/xcrun connection
|
|
191
|
+
- Check USB debugging is enabled (Android)
|
|
192
|
+
- Ensure device is unlocked
|
|
193
|
+
|
|
194
|
+
**Connection refused errors:**
|
|
195
|
+
- Check ADB/xcrun connection
|
|
196
|
+
|
|
197
|
+
**API authentication errors:**
|
|
198
|
+
- Verify `MINITAP_API_KEY` is set correctly
|
|
199
|
+
|
|
200
|
+
## Links
|
|
201
|
+
|
|
202
|
+
- **Mobile-Use SDK:** [github.com/minitap-ai/mobile-use](https://github.com/minitap-ai/mobile-use)
|
|
203
|
+
- **Mobile-Use Documentation:** [docs.minitap.ai](https://docs.minitap.ai)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
minitap/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
minitap/mcp/core/agents/compare_screenshots.md,sha256=Gt27HVzXzu71BxcanKPokz1dFPvq90vXbjE2HOn5X0I,3559
|
|
3
|
+
minitap/mcp/core/agents/compare_screenshots.py,sha256=Yb7kR8Cv0gWzXyNf-6IS7_9l1npfqYmL-SONJJGgzM4,2060
|
|
4
|
+
minitap/mcp/core/agents/extract_figma_assets.md,sha256=JrXuWF8-2PeQpVix-kf-p6zmu2gQVf9Z6ptTK1cedDk,2413
|
|
5
|
+
minitap/mcp/core/agents/extract_figma_assets.py,sha256=WAmn4CvN1ONJkJp2KH9l080hhZ_ge0Pdan6ejk_GWOo,2038
|
|
6
|
+
minitap/mcp/core/config.py,sha256=gfx-cXJsgB_W2dSNHnb5jeWEYfe3VBZUMeF1nbNAdiQ,962
|
|
7
|
+
minitap/mcp/core/decorators.py,sha256=iekv181o_rkv0upacFWkmPqxsZRTzuLFyOZ0sIDtQnQ,1317
|
|
8
|
+
minitap/mcp/core/device.py,sha256=sEO3Z-8F325hDOObdH1YBhZE60f17FmIclt5UlhY_nU,7875
|
|
9
|
+
minitap/mcp/core/llm.py,sha256=z_pYZkZcAchsiWPh4W79frQPANsfYyFPUe8DJo8lZO0,822
|
|
10
|
+
minitap/mcp/core/models.py,sha256=egLScxPAMo4u5cqY33UKba7z7DsdgqfPW409UAqW1Jg,1942
|
|
11
|
+
minitap/mcp/core/sdk_agent.py,sha256=-9l1YetD93dzxOeSFOT_j8dDfDFjhJLiir8bhzEjI3Y,900
|
|
12
|
+
minitap/mcp/core/utils.py,sha256=3uExpRoh7affIieZx3TLlZTmZCcoxWfx1YpPbwhjiJY,1791
|
|
13
|
+
minitap/mcp/main.py,sha256=B7KE6_5UNGKanS0WMJYBq8vp0HE_Lr0BG9KR4BwYxwU,4341
|
|
14
|
+
minitap/mcp/server/middleware.py,sha256=fbry_IiHmwUxVjsWgOU2goybcS1kLRXFZZ89KPH1d8E,880
|
|
15
|
+
minitap/mcp/server/poller.py,sha256=Qakq4yO3EJ9dXmRqtE3sJjyk0ij7VBU-NuupHhTf37g,2539
|
|
16
|
+
minitap/mcp/tools/analyze_screen.py,sha256=fjcjf3tTZDlxzmiQFHFNgw38bxPz4eisw57zuxshN2A,1984
|
|
17
|
+
minitap/mcp/tools/compare_screenshot_with_figma.py,sha256=G69F6vRFI2tE2wW-oFYPjnY8oFMD9nRZH0H-yvtD4gE,4575
|
|
18
|
+
minitap/mcp/tools/execute_mobile_command.py,sha256=qY3UfcDq1BtYcny1YlEF4WV9LwUJxLAmLJCm1VBzxS8,2442
|
|
19
|
+
minitap/mcp/tools/go_back.py,sha256=lEmADkDkXu8JGm-sY7zL7M6GlBy-lD7Iffv4yzwoQfo,1301
|
|
20
|
+
minitap/mcp/tools/save_figma_assets.py,sha256=EN0u0TkCUXoz8guehxm-CywKYYmZFg_d4x35eTNAovQ,9182
|
|
21
|
+
minitap/mcp/tools/screen_analyzer.md,sha256=TTO80JQWusbA9cKAZn-9cqhgVHm6F_qJh5w152hG3YM,734
|
|
22
|
+
minitap_mcp-0.4.0.dist-info/WHEEL,sha256=5w2T7AS2mz1-rW9CNagNYWRCaB0iQqBMYLwKdlgiR4Q,78
|
|
23
|
+
minitap_mcp-0.4.0.dist-info/entry_points.txt,sha256=rYVoXm7tSQCqQTtHx4Lovgn1YsjwtEEHfddKrfEVHuY,55
|
|
24
|
+
minitap_mcp-0.4.0.dist-info/METADATA,sha256=27wi_Bedtm971es6fHljAF8wE45uCkSoYYwwqqFvmY0,5885
|
|
25
|
+
minitap_mcp-0.4.0.dist-info/RECORD,,
|
minitap/mcp/core/agents.py
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
from minitap.mobile_use.sdk import Agent
|
|
4
|
-
from minitap.mobile_use.sdk.builders import Builders
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def get_mobile_use_agent():
|
|
8
|
-
config = Builders.AgentConfig
|
|
9
|
-
custom_adb_socket = os.getenv("ADB_SERVER_SOCKET")
|
|
10
|
-
if custom_adb_socket:
|
|
11
|
-
parts = custom_adb_socket.split(":")
|
|
12
|
-
if len(parts) != 3:
|
|
13
|
-
raise ValueError(f"Invalid ADB server socket: {custom_adb_socket}")
|
|
14
|
-
_, host, port = parts
|
|
15
|
-
config = config.with_adb_server(host=host, port=int(port))
|
|
16
|
-
return Agent(config=config.build())
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
agent = get_mobile_use_agent()
|