minitap-mcp 0.5.3__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.
@@ -1,58 +0,0 @@
1
- from pathlib import Path
2
- from uuid import uuid4
3
-
4
- from jinja2 import Template
5
- from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
6
- from pydantic import Field
7
-
8
- from minitap.mcp.core.config import settings
9
- from minitap.mcp.core.decorators import handle_tool_errors
10
- from minitap.mcp.core.device import capture_screenshot, find_mobile_device
11
- from minitap.mcp.core.llm import get_minitap_llm
12
- from minitap.mcp.core.utils.images import compress_base64_jpeg, get_screenshot_message_for_llm
13
- from minitap.mcp.main import mcp
14
-
15
-
16
- @mcp.tool(
17
- name="analyze_screen",
18
- description="""
19
- Analyze what is shown on the mobile device screen.
20
- This tool takes a screenshot file path and uses a vision-capable LLM
21
- to analyze and describe what's on the screen. Useful for understanding
22
- UI elements, extracting text, or identifying specific features.
23
- """,
24
- )
25
- @handle_tool_errors
26
- async def analyze_screen(
27
- prompt: str = Field(
28
- description="Prompt for the analysis.",
29
- ),
30
- device_id: str | None = Field(
31
- default=None,
32
- description="ID of the device screen to analyze. "
33
- "If not provided, the first available device is taken.",
34
- ),
35
- ) -> str | list | dict:
36
- system_message = Template(
37
- Path(__file__).parent.joinpath("screen_analyzer.md").read_text(encoding="utf-8")
38
- ).render()
39
-
40
- # Find the device and capture screenshot
41
- device = find_mobile_device(device_id=device_id)
42
- screenshot_base64 = capture_screenshot(device)
43
- compressed_image_base64 = compress_base64_jpeg(screenshot_base64)
44
-
45
- messages: list[BaseMessage] = [
46
- SystemMessage(content=system_message),
47
- get_screenshot_message_for_llm(compressed_image_base64),
48
- HumanMessage(content=prompt),
49
- ]
50
-
51
- llm = get_minitap_llm(
52
- trace_id=str(uuid4()),
53
- remote_tracing=True,
54
- model=settings.VISION_MODEL,
55
- temperature=1,
56
- )
57
- response = await llm.ainvoke(messages)
58
- return response.content
@@ -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
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))
@@ -1,30 +0,0 @@
1
- minitap/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- minitap/mcp/core/agents/compare_screenshots/agent.py,sha256=k25xbXJSMZgWYsDP0qpmNcqBo5yGbsQxRZ0nnYKHgbc,2054
3
- minitap/mcp/core/agents/compare_screenshots/eval/prompts/prompt_1.md,sha256=qAyqOroSJROgrvlbsLCtiwFyBKuIMCQ-720A5cwgwPY,3563
4
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/actual.png,sha256=woKE-aTTdb-9ArqfnV-xKKyit1Fu_hklavVwAaMQ14E,99455
5
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/figma.png,sha256=ghxi1P-ofnmMv5_ASm0Rzo5ll_C8-E6ojQUCHRR33TA,102098
6
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/human_feedback.txt,sha256=0gVEqIFpmCX1cx1tlGBdfqDGbTeedyVfwTJZqePURkA,700
7
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/model_params.json,sha256=p93kbUGxLYwc4NAEzPTBDA2XZ1ucsa7yevXZRe6V4Mc,37
8
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/output.md,sha256=Q0_5w9x5WAMqx6-I7KgjlS-tt7HnWKIunP43J7F0W_o,3703
9
- minitap/mcp/core/agents/compare_screenshots/prompt.md,sha256=qAyqOroSJROgrvlbsLCtiwFyBKuIMCQ-720A5cwgwPY,3563
10
- minitap/mcp/core/config.py,sha256=_rIH31treZlM2RVnTz5cPXhV9Bu4D-w4TmbPh5_mxxM,1026
11
- minitap/mcp/core/decorators.py,sha256=kMx_mlaa-2U1AgCoYkgPoLOa-iOoKUF1OjcNV7x59Ds,2940
12
- minitap/mcp/core/device.py,sha256=sEO3Z-8F325hDOObdH1YBhZE60f17FmIclt5UlhY_nU,7875
13
- minitap/mcp/core/llm.py,sha256=tI5m5rFDLeMkXE5WExnzYSzHU3nTIEiSC9nAsPzVMaU,1144
14
- minitap/mcp/core/logging_config.py,sha256=OJlArPJxflbhckerFsRHVTzy3jwsLsNSPN0LVpkmpNM,1861
15
- minitap/mcp/core/models.py,sha256=egLScxPAMo4u5cqY33UKba7z7DsdgqfPW409UAqW1Jg,1942
16
- minitap/mcp/core/sdk_agent.py,sha256=-9l1YetD93dzxOeSFOT_j8dDfDFjhJLiir8bhzEjI3Y,900
17
- minitap/mcp/core/utils/figma.py,sha256=L5aAHm59mrRYaqrwMJSM24SSdZPu2yVg-wsHTF3L8vk,2310
18
- minitap/mcp/core/utils/images.py,sha256=3uExpRoh7affIieZx3TLlZTmZCcoxWfx1YpPbwhjiJY,1791
19
- minitap/mcp/main.py,sha256=RsFAU32Rgrt66OiOtO74k7HSeTWRqnw31IurbnsOI3I,4811
20
- minitap/mcp/server/middleware.py,sha256=fbry_IiHmwUxVjsWgOU2goybcS1kLRXFZZ89KPH1d8E,880
21
- minitap/mcp/server/poller.py,sha256=Qakq4yO3EJ9dXmRqtE3sJjyk0ij7VBU-NuupHhTf37g,2539
22
- minitap/mcp/tools/analyze_screen.py,sha256=xQALhVfbEn13ao7C3EvzuBusOgjYIkS9hzKhzQSne6g,1991
23
- minitap/mcp/tools/compare_screenshot_with_figma.py,sha256=a3rHi8MbomgspJ2iPPgTyRoYrcEai2r4ED_-DssSbNI,4581
24
- minitap/mcp/tools/execute_mobile_command.py,sha256=iZLQHu-NGSjtjIjzYLTf2Da0t--RgjcFghmUBfhmo1I,2484
25
- minitap/mcp/tools/save_figma_assets.py,sha256=V1gnQsJ1tciOxiK08aaqQxOEerJkKzxU8r4hJmkXHtA,9945
26
- minitap/mcp/tools/screen_analyzer.md,sha256=TTO80JQWusbA9cKAZn-9cqhgVHm6F_qJh5w152hG3YM,734
27
- minitap_mcp-0.5.3.dist-info/WHEEL,sha256=ZHijuPszqKbNczrBXkSuoxdxocbxgFghqnequ9ZQlVk,79
28
- minitap_mcp-0.5.3.dist-info/entry_points.txt,sha256=rYVoXm7tSQCqQTtHx4Lovgn1YsjwtEEHfddKrfEVHuY,55
29
- minitap_mcp-0.5.3.dist-info/METADATA,sha256=D6x7r5Lvd_L9OEjDxkRZJPx42kIexF_gK3Ovxf0q4K0,8827
30
- minitap_mcp-0.5.3.dist-info/RECORD,,