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
|
@@ -1,132 +0,0 @@
|
|
|
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.agent 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 (supports both local and cloud devices)
|
|
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)}")
|
|
@@ -1,276 +0,0 @@
|
|
|
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.config import settings
|
|
15
|
-
from minitap.mcp.core.decorators import handle_tool_errors
|
|
16
|
-
from minitap.mcp.core.logging_config import get_logger
|
|
17
|
-
from minitap.mcp.core.models import (
|
|
18
|
-
AssetDownloadResult,
|
|
19
|
-
AssetDownloadSummary,
|
|
20
|
-
DownloadStatus,
|
|
21
|
-
FigmaDesignContextOutput,
|
|
22
|
-
)
|
|
23
|
-
from minitap.mcp.core.utils.figma import ExtractedAssets, FigmaAsset, extract_figma_assets
|
|
24
|
-
from minitap.mcp.main import mcp
|
|
25
|
-
from minitap.mcp.tools.compare_screenshot_with_figma import (
|
|
26
|
-
compress_image_base64,
|
|
27
|
-
get_figma_screenshot,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
logger = get_logger(__name__)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@mcp.tool(
|
|
34
|
-
name="save_figma_assets",
|
|
35
|
-
description="""
|
|
36
|
-
Fetch Figma design assets/react implementation code and save them locally in the workspace.
|
|
37
|
-
|
|
38
|
-
This tool:
|
|
39
|
-
1. Calls get_design_context from Figma MCP to get the React/TypeScript code
|
|
40
|
-
2. Extracts asset URLs and transforms const declarations to import statements
|
|
41
|
-
3. Downloads each asset to .mobile-use/figma_assets/<node-id>/ folder
|
|
42
|
-
4. Saves the transformed code to .mobile-use/figma_assets/<node-id>/code_implementation.ts
|
|
43
|
-
5. Returns a list of downloaded files
|
|
44
|
-
""",
|
|
45
|
-
)
|
|
46
|
-
@handle_tool_errors
|
|
47
|
-
async def save_figma_assets(
|
|
48
|
-
node_id: str = Field(
|
|
49
|
-
description=(
|
|
50
|
-
"The node ID of the Figma design. Expected format is ':' separated.\n"
|
|
51
|
-
"Example: If given the URL https://figma.com/design/:fileKey/:fileName?node-id=1-2,\n"
|
|
52
|
-
"the extracted nodeId would be 1:2. Strictly respect this format."
|
|
53
|
-
)
|
|
54
|
-
),
|
|
55
|
-
file_key: str = Field(
|
|
56
|
-
description=(
|
|
57
|
-
"The file key of the Figma file.\n"
|
|
58
|
-
"Example: If given the URL https://figma.com/design/abc123/MyFile?node-id=1-2,\n"
|
|
59
|
-
"the extracted fileKey would be 'abc123'."
|
|
60
|
-
)
|
|
61
|
-
),
|
|
62
|
-
workspace_path: str = Field(
|
|
63
|
-
default=".",
|
|
64
|
-
description=(
|
|
65
|
-
"The workspace path where assets should be saved. Defaults to current directory."
|
|
66
|
-
),
|
|
67
|
-
),
|
|
68
|
-
) -> ToolResult:
|
|
69
|
-
"""Fetch and save Figma assets locally."""
|
|
70
|
-
|
|
71
|
-
# Step 1: Get design context from Figma MCP
|
|
72
|
-
design_context = await get_design_context(node_id, file_key)
|
|
73
|
-
|
|
74
|
-
# Step 2: Extract asset URLs and transform code
|
|
75
|
-
extracted_context: ExtractedAssets = extract_figma_assets(design_context.code_implementation)
|
|
76
|
-
if not extracted_context.assets:
|
|
77
|
-
raise ToolError("No assets found in the Figma design context.")
|
|
78
|
-
|
|
79
|
-
# Step 3: Create directory structure
|
|
80
|
-
# Convert node_id format (1:2) to folder name (1-2)
|
|
81
|
-
folder_name = node_id.replace(":", "-")
|
|
82
|
-
assets_dir = Path(workspace_path) / ".mobile-use" / "figma_assets" / folder_name
|
|
83
|
-
|
|
84
|
-
# Delete existing directory to remove stale assets
|
|
85
|
-
if assets_dir.exists():
|
|
86
|
-
shutil.rmtree(assets_dir)
|
|
87
|
-
|
|
88
|
-
# Create fresh directory
|
|
89
|
-
assets_dir.mkdir(parents=True, exist_ok=True)
|
|
90
|
-
|
|
91
|
-
# Step 4: Download assets with resilient error handling
|
|
92
|
-
download_summary = AssetDownloadSummary()
|
|
93
|
-
|
|
94
|
-
for idx, asset in enumerate(extracted_context.assets, 1):
|
|
95
|
-
logger.debug(
|
|
96
|
-
"Downloading asset",
|
|
97
|
-
idx=idx,
|
|
98
|
-
total_count=len(extracted_context.assets),
|
|
99
|
-
variable_name=asset.variable_name,
|
|
100
|
-
extension=asset.extension,
|
|
101
|
-
)
|
|
102
|
-
result = download_asset(asset, assets_dir)
|
|
103
|
-
if result.status == DownloadStatus.SUCCESS:
|
|
104
|
-
logger.debug(
|
|
105
|
-
"Asset downloaded successfully",
|
|
106
|
-
idx=idx,
|
|
107
|
-
variable_name=asset.variable_name,
|
|
108
|
-
extension=asset.extension,
|
|
109
|
-
)
|
|
110
|
-
download_summary.successful.append(result)
|
|
111
|
-
else:
|
|
112
|
-
logger.debug(
|
|
113
|
-
"Asset download failed",
|
|
114
|
-
idx=idx,
|
|
115
|
-
variable_name=asset.variable_name,
|
|
116
|
-
extension=asset.extension,
|
|
117
|
-
error=result.error,
|
|
118
|
-
)
|
|
119
|
-
download_summary.failed.append(result)
|
|
120
|
-
|
|
121
|
-
# Step 4.5: Save code implementation
|
|
122
|
-
code_implementation_file = assets_dir / "code_implementation.ts"
|
|
123
|
-
|
|
124
|
-
commented_code_implementation_guidelines = ""
|
|
125
|
-
if design_context.code_implementation_guidelines:
|
|
126
|
-
commented_code_implementation_guidelines = "\n".join(
|
|
127
|
-
["// " + line for line in design_context.code_implementation_guidelines.split("\n")]
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
commented_nodes_guidelines = ""
|
|
131
|
-
if design_context.nodes_guidelines:
|
|
132
|
-
commented_nodes_guidelines = "\n".join(
|
|
133
|
-
["// " + line for line in design_context.nodes_guidelines.split("\n")]
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
code_implementation_file.write_text(
|
|
137
|
-
extracted_context.code_implementation
|
|
138
|
-
+ "\n\n"
|
|
139
|
-
+ commented_code_implementation_guidelines
|
|
140
|
-
+ "\n\n"
|
|
141
|
-
+ commented_nodes_guidelines,
|
|
142
|
-
encoding="utf-8",
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
# Step 5: Generate friendly output message
|
|
146
|
-
result_parts = []
|
|
147
|
-
|
|
148
|
-
if download_summary.successful:
|
|
149
|
-
result_parts.append(
|
|
150
|
-
f"✅ Successfully downloaded {download_summary.success_count()} asset(s) "
|
|
151
|
-
f"to .mobile-use/figma_assets/{folder_name}/:\n"
|
|
152
|
-
)
|
|
153
|
-
for asset_result in download_summary.successful:
|
|
154
|
-
result_parts.append(f" • {asset_result.filename}")
|
|
155
|
-
|
|
156
|
-
if download_summary.failed:
|
|
157
|
-
result_parts.append(
|
|
158
|
-
f"\n\n⚠️ Failed to download {download_summary.failure_count()} asset(s):"
|
|
159
|
-
)
|
|
160
|
-
for asset_result in download_summary.failed:
|
|
161
|
-
error_msg = f": {asset_result.error}" if asset_result.error else ""
|
|
162
|
-
result_parts.append(f" • {asset_result.filename}{error_msg}")
|
|
163
|
-
|
|
164
|
-
if code_implementation_file.exists():
|
|
165
|
-
result_parts.append(
|
|
166
|
-
f"\n\n✅ Successfully saved code implementation to {code_implementation_file.name}"
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
expected_screenshot = await get_figma_screenshot(node_id)
|
|
170
|
-
compressed_expected = compress_image_base64(expected_screenshot)
|
|
171
|
-
|
|
172
|
-
return ToolResult(
|
|
173
|
-
content=[
|
|
174
|
-
mcp_ref.types.TextContent(
|
|
175
|
-
type="text",
|
|
176
|
-
text="\n".join(result_parts),
|
|
177
|
-
),
|
|
178
|
-
mcp_ref.types.TextContent(
|
|
179
|
-
type="text",
|
|
180
|
-
text="**Expected (Figma design)**",
|
|
181
|
-
),
|
|
182
|
-
mcp_ref.types.ImageContent(
|
|
183
|
-
type="image",
|
|
184
|
-
data=compressed_expected,
|
|
185
|
-
mimeType="image/jpeg",
|
|
186
|
-
),
|
|
187
|
-
]
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
async def get_design_context(node_id: str, file_key: str) -> FigmaDesignContextOutput:
|
|
192
|
-
"""Fetch design context from Figma MCP server.
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
node_id: The Figma node ID in format "1:2"
|
|
196
|
-
file_key: The Figma file key
|
|
197
|
-
|
|
198
|
-
Returns:
|
|
199
|
-
The React/TypeScript code as a string
|
|
200
|
-
|
|
201
|
-
Raises:
|
|
202
|
-
ToolError: If fetching fails
|
|
203
|
-
"""
|
|
204
|
-
try:
|
|
205
|
-
async with Client(settings.FIGMA_MCP_SERVER_URL) as client:
|
|
206
|
-
result: CallToolResult = await client.call_tool(
|
|
207
|
-
"get_design_context",
|
|
208
|
-
{
|
|
209
|
-
"nodeId": node_id,
|
|
210
|
-
"fileKey": file_key,
|
|
211
|
-
"clientLanguages": "typescript",
|
|
212
|
-
"clientFrameworks": "react",
|
|
213
|
-
},
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
code_implementation = ""
|
|
217
|
-
code_implementation_guidelines = None
|
|
218
|
-
nodes_guidelines = None
|
|
219
|
-
|
|
220
|
-
if len(result.content) > 0 and isinstance(result.content[0], mcp_ref.types.TextContent):
|
|
221
|
-
code_implementation = result.content[0].text
|
|
222
|
-
else:
|
|
223
|
-
raise ToolError("Failed to fetch design context from Figma")
|
|
224
|
-
|
|
225
|
-
if len(result.content) > 1:
|
|
226
|
-
if isinstance(result.content[1], mcp_ref.types.TextContent):
|
|
227
|
-
code_implementation_guidelines = result.content[1].text
|
|
228
|
-
if len(result.content) > 2 and isinstance(result.content[2], mcp_ref.types.TextContent):
|
|
229
|
-
nodes_guidelines = result.content[2].text
|
|
230
|
-
|
|
231
|
-
return FigmaDesignContextOutput(
|
|
232
|
-
code_implementation=code_implementation,
|
|
233
|
-
code_implementation_guidelines=code_implementation_guidelines,
|
|
234
|
-
nodes_guidelines=nodes_guidelines,
|
|
235
|
-
)
|
|
236
|
-
except Exception as e:
|
|
237
|
-
raise ToolError(
|
|
238
|
-
f"Failed to fetch design context from Figma: {str(e)}.\n"
|
|
239
|
-
"Ensure the Figma MCP server is running through the official Figma desktop app."
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def download_asset(asset: FigmaAsset, assets_dir: Path) -> AssetDownloadResult:
|
|
244
|
-
"""Download a single asset with error handling.
|
|
245
|
-
|
|
246
|
-
Args:
|
|
247
|
-
asset: FigmaAsset model with variable_name, url, and extension
|
|
248
|
-
assets_dir: Directory to save the asset
|
|
249
|
-
|
|
250
|
-
Returns:
|
|
251
|
-
AssetDownloadResult with status and optional error message
|
|
252
|
-
"""
|
|
253
|
-
variable_name = asset.variable_name
|
|
254
|
-
url = asset.url
|
|
255
|
-
extension = asset.extension
|
|
256
|
-
|
|
257
|
-
# Convert camelCase variable name to filename
|
|
258
|
-
# e.g., imgSignal -> imgSignal.svg
|
|
259
|
-
filename = f"{variable_name}.{extension}"
|
|
260
|
-
filepath = assets_dir / filename
|
|
261
|
-
|
|
262
|
-
try:
|
|
263
|
-
response = requests.get(url, timeout=30)
|
|
264
|
-
if response.status_code == 200:
|
|
265
|
-
filepath.write_bytes(response.content)
|
|
266
|
-
return AssetDownloadResult(filename=filename, status=DownloadStatus.SUCCESS)
|
|
267
|
-
else:
|
|
268
|
-
return AssetDownloadResult(
|
|
269
|
-
filename=filename,
|
|
270
|
-
status=DownloadStatus.FAILED,
|
|
271
|
-
error=f"HTTP {response.status_code}",
|
|
272
|
-
)
|
|
273
|
-
except requests.exceptions.Timeout:
|
|
274
|
-
return AssetDownloadResult(filename=filename, status=DownloadStatus.FAILED, error="Timeout")
|
|
275
|
-
except Exception as e:
|
|
276
|
-
return AssetDownloadResult(filename=filename, status=DownloadStatus.FAILED, error=str(e))
|
|
File without changes
|