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.
@@ -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))