minitap-mcp 0.3.0__py3-none-any.whl → 0.4.1__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.
@@ -0,0 +1,62 @@
1
+ You will be given _two screenshots_.
2
+
3
+ 1. "Expected screenshot" — this is the design from Figma.
4
+ 2. "Implemented screenshot" — this is the actual phone screen that has been built.
5
+
6
+ Your task is to **compare the two screenshots** in detail, and generate a structured report that includes:
7
+
8
+ - A comprehensive list of **all visible differences** between the expected design and the implemented screen.
9
+ - For each difference, provide:
10
+ - A clear **description** of what changed (for example: "The 'Submit' button label changed from 'Submit' to 'Send'", "The icon moved 8px to the right", "The background colour of header changed from #FFFFFF to #F6F6F6", etc.).
11
+ - The **type of change** (e.g., text change, color change, position/movement, size change, added element, removed element, style change).
12
+ - The **location** of the change (for example: "bottom-centre of screen", "top header area", "to the right of search bar"). If possible, approximate coordinates or bounding box (e.g., "approx. 240×180 px at screen width 1080").
13
+ - The **impact on implementation** (i.e., reasoning about what this means: "The implemented version uses a different text label – so behaviour may differ", "The icon moved and may overlap another element", etc.).
14
+ - A **recommendation** if relevant (e.g., "Should revert to #FFFFFF to match design", "Check alignment of icon relative to search bar", etc.).
15
+
16
+ **Important**:
17
+
18
+ - Assume the screenshots are aligned (same resolution and scale); if not aligned mention that as a difference.
19
+ - Focus on _visible UI differences_ (layout, text, style, iconography) – you do _not_ need to inspect source code, only what is visually rendered.
20
+ - Do _not_ produce generic comments like "looks like a difference" – aim for _precise, actionable descriptions_.
21
+ - **IGNORE dynamic/personal content** that naturally differs between mockups and real implementations:
22
+ - User profile information (names, usernames, email addresses, profile pictures)
23
+ - Time-based information (current time, dates, timestamps, "2 hours ago", etc.)
24
+ - Dynamic data (notification counts, unread badges, live statistics)
25
+ - Sample/placeholder content that varies (e.g., "John Doe" vs "Jane Smith")
26
+ - System status information (battery level, signal strength, network indicators)
27
+ - Only flag these as differences if the _structure, layout, or styling_ of these elements differs, not the content itself.
28
+ - Output in a structured format, for example:
29
+
30
+ ```
31
+
32
+ 1. Location: [top header – full width]
33
+ Change: Background colour changed from #FFFFFF → #F6F6F6
34
+ Type: Colour change
35
+ Impact: The header will appear darker than design; text contrast may be lower.
36
+ Recommendation: Update header background to #FFFFFF as in design.
37
+
38
+ ```
39
+
40
+ - At the end produce a summary with ONLY:
41
+ - Total number of differences found
42
+ - Overall "match score" out of 100 (your estimation of how closely the implementation matches the design)
43
+ - Do NOT include any recap, overview, or macro-level summary of changes - all details are already captured in the differences list above.
44
+
45
+ ### Input:
46
+
47
+ - Screenshot A: Expected (Figma)
48
+ - Screenshot B: Implemented (Phone)
49
+ Provide both screenshots and then the prompt.
50
+
51
+ ### Output:
52
+
53
+ Structured list of differences + summary.
54
+
55
+ Please use the following to start the analysis.
56
+ **Input:**
57
+ First screen is the Figma screenshot (what is expected)
58
+ Second screen is what is expected (taken from the phone, after the implementation)
59
+
60
+ You will have this data in the next messages sent by the user.
61
+
62
+ Go ahead and generate your report.
@@ -0,0 +1,65 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+ from uuid import uuid4
4
+
5
+ from jinja2 import Template
6
+ from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
7
+ from pydantic import BaseModel
8
+
9
+ from minitap.mcp.core.device import capture_screenshot, find_mobile_device
10
+ from minitap.mcp.core.llm import get_minitap_llm
11
+ from minitap.mcp.core.utils import get_screenshot_message_for_llm
12
+
13
+
14
+ class CompareScreenshotsOutput(BaseModel):
15
+ comparison_text: str
16
+ expected_screenshot_base64: str
17
+ current_screenshot_base64: str
18
+
19
+
20
+ async def compare_screenshots(
21
+ expected_screenshot_base64: str,
22
+ ) -> CompareScreenshotsOutput:
23
+ """
24
+ Compare screenshots and return the comparison text along with both screenshots.
25
+
26
+ Returns:
27
+ CompareScreenshotsOutput
28
+ """
29
+ system_message = Template(
30
+ Path(__file__).parent.joinpath("compare_screenshots.md").read_text(encoding="utf-8")
31
+ ).render()
32
+
33
+ device = find_mobile_device()
34
+ current_screenshot = capture_screenshot(device)
35
+
36
+ messages: list[BaseMessage] = [
37
+ SystemMessage(content=system_message),
38
+ HumanMessage(content="Here is the Figma screenshot (what needs to be matched):"),
39
+ get_screenshot_message_for_llm(expected_screenshot_base64),
40
+ HumanMessage(content="Here is the screenshot of the mobile device:"),
41
+ get_screenshot_message_for_llm(current_screenshot),
42
+ ]
43
+
44
+ llm = get_minitap_llm(
45
+ trace_id=str(uuid4()),
46
+ remote_tracing=True,
47
+ model="google/gemini-2.5-pro",
48
+ temperature=1,
49
+ )
50
+ response = await llm.ainvoke(messages)
51
+ return CompareScreenshotsOutput(
52
+ comparison_text=str(response.content),
53
+ expected_screenshot_base64=expected_screenshot_base64,
54
+ current_screenshot_base64=current_screenshot,
55
+ )
56
+
57
+
58
+ async def main():
59
+ expected_screenshot_base64 = "Base64 encoded screenshot to compare with."
60
+ result = await compare_screenshots(expected_screenshot_base64)
61
+ print(result.model_dump_json(indent=2))
62
+
63
+
64
+ if __name__ == "__main__":
65
+ asyncio.run(main())
@@ -0,0 +1,64 @@
1
+ You are an expert at parsing React/TypeScript code to extract asset URLs and generate clean, documented code implementations.
2
+
3
+ Your task is to:
4
+
5
+ 1. Extract all asset URLs from the provided code snippet
6
+ 2. Generate a clean `code_implementation` output that includes the React code with embedded comments referencing implementation and node guidelines
7
+
8
+ **Instructions:**
9
+
10
+ ## Part 1: Extract Asset URLs
11
+
12
+ 1. Look for all constant declarations that contain URLs pointing to assets (images, SVGs, etc.)
13
+ 2. These constants typically follow patterns like:
14
+
15
+ - `const imgVariableName = "http://localhost:3845/assets/[hash].[extension]";`
16
+ - The variable names usually start with `img` followed by a descriptive name in camelCase
17
+
18
+ 3. For each asset URL found, extract:
19
+ - The **variable name** (e.g., `imgSignal`, `imgBatteryThreeQuarters`)
20
+ - The **full URL** (e.g., `http://localhost:3845/assets/685c5ac58caa29556e29737cf8f8c9605d9c8571.svg`)
21
+ - The **file extension** from the URL (e.g., `svg`, `png`, `jpg`)
22
+
23
+ ## Part 2: Generate Code Implementation
24
+
25
+ The `code_implementation` field should contain:
26
+
27
+ 1. The React/TypeScript code with **LOCAL asset imports** instead of HTTP URLs:
28
+
29
+ - Convert `const imgSignal = "http://localhost:3845/assets/[hash].svg";`
30
+ - To `import imgSignal from './assets/imgSignal.svg';` (or appropriate relative path)
31
+ - Use the **exact same variable names** as in the original const declarations
32
+ - **CRITICAL**: Preserve the variable naming convention
33
+
34
+ 2. Preserve all `data-node-id` attributes and other metadata in the code
35
+
36
+ ## Part 3: Return Format
37
+
38
+ Return a JSON object with two fields:
39
+
40
+ - `assets`: Array of extracted asset objects
41
+ - `code_implementation`: String containing the React code with embedded guideline comments
42
+
43
+ ```json
44
+ {
45
+ "assets": [
46
+ {
47
+ "variable_name": "imgSignal",
48
+ "url": "http://localhost:3845/assets/685c5ac58caa29556e29737cf8f8c9605d9c8571.svg",
49
+ "extension": "svg"
50
+ },
51
+ ...
52
+ ],
53
+ "code_implementation": "import ... function ..."
54
+ }
55
+ ```
56
+
57
+ **Important:**
58
+
59
+ - Only extract asset URLs
60
+ - Preserve the exact variable names as they appear in the code
61
+ - DO NOT MISS any assets
62
+ - If no assets are found, return an empty array for `assets`
63
+ - Return ONLY the JSON object with both `assets` and `code_implementation` fields
64
+ - Do NOT include the const declarations of the assets in the code_implementation output - convert them to imports.
@@ -0,0 +1,96 @@
1
+ """Agent to extract Figma asset URLs from design context code."""
2
+
3
+ import re
4
+ import uuid
5
+ from pathlib import Path
6
+
7
+ from jinja2 import Template
8
+ from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
9
+ from pydantic import BaseModel, Field
10
+
11
+ from minitap.mcp.core.llm import get_minitap_llm
12
+
13
+
14
+ class FigmaAsset(BaseModel):
15
+ """Represents a single Figma asset."""
16
+
17
+ variable_name: str = Field(description="The variable name from the code (e.g., imgSignal)")
18
+ url: str = Field(description="The full URL to the asset")
19
+ extension: str = Field(description="The file extension (e.g., svg, png, jpg)")
20
+
21
+
22
+ class ExtractedAssets(BaseModel):
23
+ """Container for all extracted Figma assets."""
24
+
25
+ assets: list[FigmaAsset] = Field(
26
+ default_factory=list,
27
+ description="List of all extracted assets from the Figma design context",
28
+ )
29
+ code_implementation: str = Field(
30
+ description=(
31
+ "The React/TypeScript code\n"
32
+ "with the local url declarations turned into const declarations"
33
+ )
34
+ )
35
+
36
+
37
+ def sanitize_unicode_for_llm(text: str) -> str:
38
+ """Remove or replace problematic Unicode characters that increase token consumption.
39
+
40
+ Characters outside the Basic Multilingual Plane (BMP) like emoji and special symbols
41
+ get escaped as \\U sequences when sent to LLMs, dramatically increasing token count
42
+ and processing time.
43
+
44
+ Args:
45
+ text: The text to sanitize
46
+
47
+ Returns:
48
+ Text with problematic Unicode characters replaced with placeholders
49
+ """
50
+
51
+ # Replace characters outside BMP (U+10000 and above) with a placeholder
52
+ # These are typically emoji, special symbols, or rare characters
53
+ def replace_high_unicode(match):
54
+ char = match.group(0)
55
+ codepoint = ord(char)
56
+ # Return a descriptive placeholder
57
+ return f"[U+{codepoint:X}]"
58
+
59
+ # Pattern matches characters with codepoints >= U+10000
60
+ pattern = re.compile(r"[\U00010000-\U0010FFFF]")
61
+ sanitized = pattern.sub(replace_high_unicode, text)
62
+
63
+ return sanitized
64
+
65
+
66
+ async def extract_figma_assets(design_context_code: str) -> ExtractedAssets:
67
+ """Extract asset URLs from Figma design context code.
68
+
69
+ Args:
70
+ design_context_code: The React/TypeScript code from get_design_context
71
+
72
+ Returns:
73
+ List of dictionaries containing variable_name, url, and extension
74
+ """
75
+ system_message = Template(
76
+ Path(__file__).parent.joinpath("extract_figma_assets.md").read_text(encoding="utf-8")
77
+ ).render()
78
+
79
+ sanitized_code = sanitize_unicode_for_llm(design_context_code)
80
+
81
+ messages: list[BaseMessage] = [
82
+ SystemMessage(content=system_message),
83
+ HumanMessage(
84
+ content=f"Here is the code to analyze:\n\n```typescript\n{sanitized_code}\n```"
85
+ ),
86
+ ]
87
+
88
+ llm = get_minitap_llm(
89
+ model="openai/gpt-5",
90
+ temperature=0,
91
+ trace_id=str(uuid.uuid4()),
92
+ remote_tracing=True,
93
+ ).with_structured_output(ExtractedAssets)
94
+ result: ExtractedAssets = await llm.ainvoke(messages) # type: ignore
95
+
96
+ return result
@@ -16,9 +16,13 @@ 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
+ OPEN_ROUTER_API_KEY: SecretStr | None = Field(default=None)
19
20
 
20
21
  VISION_MODEL: str = Field(default="qwen/qwen-2.5-vl-7b-instruct")
21
22
 
23
+ # Figma MCP server configuration
24
+ FIGMA_MCP_SERVER_URL: str = Field(default="http://127.0.0.1:3845/mcp")
25
+
22
26
  # MCP server configuration (optional, for remote access)
23
27
  MCP_SERVER_HOST: str = Field(default="0.0.0.0")
24
28
  MCP_SERVER_PORT: int = Field(default=8000)
@@ -1,38 +1,85 @@
1
1
  """Decorators for MCP tools."""
2
2
 
3
3
  import inspect
4
+ import traceback
4
5
  from collections.abc import Callable
5
6
  from functools import wraps
6
7
  from typing import Any, TypeVar
7
8
 
8
9
  from minitap.mcp.core.device import DeviceNotFoundError
10
+ from minitap.mcp.core.logging_config import get_logger
9
11
 
10
12
  F = TypeVar("F", bound=Callable[..., Any])
11
13
 
14
+ logger = get_logger(__name__)
15
+
12
16
 
13
17
  def handle_tool_errors[T: Callable[..., Any]](func: T) -> T:
14
18
  """
15
19
  Decorator that catches all exceptions in MCP tools and returns error messages.
16
20
 
17
21
  This prevents unhandled exceptions from causing infinite loops in the MCP server.
22
+ Logs all errors with structured logging for better debugging.
18
23
  """
19
24
 
20
25
  @wraps(func)
21
26
  async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
22
27
  try:
23
- return await func(*args, **kwargs)
28
+ logger.info(
29
+ "tool_called",
30
+ tool_name=func.__name__,
31
+ args_count=len(args),
32
+ kwargs_keys=list(kwargs.keys()),
33
+ )
34
+ result = await func(*args, **kwargs)
35
+ logger.info("tool_completed", tool_name=func.__name__)
36
+ return result
24
37
  except DeviceNotFoundError as e:
38
+ logger.error(
39
+ "device_not_found_error",
40
+ tool_name=func.__name__,
41
+ error=str(e),
42
+ error_type=type(e).__name__,
43
+ )
25
44
  return f"Error: {str(e)}"
26
45
  except Exception as e:
46
+ logger.error(
47
+ "tool_error",
48
+ tool_name=func.__name__,
49
+ error=str(e),
50
+ error_type=type(e).__name__,
51
+ traceback=traceback.format_exc(),
52
+ )
27
53
  return f"Error in {func.__name__}: {type(e).__name__}: {str(e)}"
28
54
 
29
55
  @wraps(func)
30
56
  def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
31
57
  try:
32
- return func(*args, **kwargs)
58
+ logger.info(
59
+ "tool_called",
60
+ tool_name=func.__name__,
61
+ args_count=len(args),
62
+ kwargs_keys=list(kwargs.keys()),
63
+ )
64
+ result = func(*args, **kwargs)
65
+ logger.info("tool_completed", tool_name=func.__name__)
66
+ return result
33
67
  except DeviceNotFoundError as e:
68
+ logger.error(
69
+ "device_not_found_error",
70
+ tool_name=func.__name__,
71
+ error=str(e),
72
+ error_type=type(e).__name__,
73
+ )
34
74
  return f"Error: {str(e)}"
35
75
  except Exception as e:
76
+ logger.error(
77
+ "tool_error",
78
+ tool_name=func.__name__,
79
+ error=str(e),
80
+ error_type=type(e).__name__,
81
+ traceback=traceback.format_exc(),
82
+ )
36
83
  return f"Error in {func.__name__}: {type(e).__name__}: {str(e)}"
37
84
 
38
85
  # Check if the function is async
minitap/mcp/core/llm.py CHANGED
@@ -26,3 +26,14 @@ def get_minitap_llm(
26
26
  },
27
27
  )
28
28
  return client
29
+
30
+
31
+ def get_openrouter_llm(model_name: str, temperature: float = 1):
32
+ assert settings.OPEN_ROUTER_API_KEY is not None
33
+ client = ChatOpenAI(
34
+ model=model_name,
35
+ temperature=temperature,
36
+ api_key=settings.OPEN_ROUTER_API_KEY,
37
+ base_url="https://openrouter.ai/api/v1",
38
+ )
39
+ return client
@@ -0,0 +1,59 @@
1
+ """Structured logging configuration using structlog."""
2
+
3
+ import logging
4
+ import sys
5
+
6
+ import structlog
7
+
8
+
9
+ def configure_logging(log_level: str = "INFO") -> None:
10
+ """Configure structlog with sensible defaults for the MCP server.
11
+
12
+ Args:
13
+ log_level: The logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
14
+ """
15
+ # Configure standard library logging
16
+ logging.basicConfig(
17
+ format="%(message)s",
18
+ stream=sys.stdout,
19
+ level=getattr(logging, log_level.upper()),
20
+ )
21
+
22
+ # Configure structlog
23
+ structlog.configure(
24
+ processors=[
25
+ # Add log level to event dict
26
+ structlog.stdlib.add_log_level,
27
+ # Add timestamp
28
+ structlog.processors.TimeStamper(fmt="iso"),
29
+ # Add caller information (file, line, function)
30
+ structlog.processors.CallsiteParameterAdder(
31
+ parameters=[
32
+ structlog.processors.CallsiteParameter.FILENAME,
33
+ structlog.processors.CallsiteParameter.LINENO,
34
+ structlog.processors.CallsiteParameter.FUNC_NAME,
35
+ ]
36
+ ),
37
+ # Stack info and exception formatting
38
+ structlog.processors.StackInfoRenderer(),
39
+ structlog.processors.format_exc_info,
40
+ # Render as JSON for structured output
41
+ structlog.processors.JSONRenderer(),
42
+ ],
43
+ wrapper_class=structlog.stdlib.BoundLogger,
44
+ context_class=dict,
45
+ logger_factory=structlog.stdlib.LoggerFactory(),
46
+ cache_logger_on_first_use=True,
47
+ )
48
+
49
+
50
+ def get_logger(name: str) -> structlog.stdlib.BoundLogger:
51
+ """Get a structured logger instance.
52
+
53
+ Args:
54
+ name: The logger name (typically __name__ of the module)
55
+
56
+ Returns:
57
+ A structlog BoundLogger instance
58
+ """
59
+ return structlog.get_logger(name)
@@ -0,0 +1,59 @@
1
+ """Core models for the MCP server."""
2
+
3
+ from enum import Enum
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class FigmaAsset(BaseModel):
9
+ """Represents a single Figma asset."""
10
+
11
+ variable_name: str = Field(description="The variable name from the code (e.g., imgSignal)")
12
+ url: str = Field(description="The full URL to the asset")
13
+ extension: str = Field(description="The file extension (e.g., svg, png, jpg)")
14
+
15
+
16
+ class FigmaDesignContextOutput(BaseModel):
17
+ """Output from Figma design context containing code and guidelines."""
18
+
19
+ code_implementation: str = Field(description="The React/TypeScript code implementation")
20
+ code_implementation_guidelines: str | None = Field(
21
+ default=None, description="Guidelines for implementing the code"
22
+ )
23
+ nodes_guidelines: str | None = Field(
24
+ default=None, description="Guidelines specific to the nodes"
25
+ )
26
+
27
+
28
+ class DownloadStatus(str, Enum):
29
+ """Status of asset download operation."""
30
+
31
+ SUCCESS = "success"
32
+ FAILED = "failed"
33
+
34
+
35
+ class AssetDownloadResult(BaseModel):
36
+ """Result of downloading a single asset."""
37
+
38
+ filename: str = Field(description="The filename of the asset")
39
+ status: DownloadStatus = Field(description="The download status")
40
+ error: str | None = Field(default=None, description="Error message if download failed")
41
+
42
+
43
+ class AssetDownloadSummary(BaseModel):
44
+ """Summary of all asset download operations."""
45
+
46
+ successful: list[AssetDownloadResult] = Field(
47
+ default_factory=list, description="List of successfully downloaded assets"
48
+ )
49
+ failed: list[AssetDownloadResult] = Field(
50
+ default_factory=list, description="List of failed asset downloads"
51
+ )
52
+
53
+ def success_count(self) -> int:
54
+ """Return the number of successful downloads."""
55
+ return len(self.successful)
56
+
57
+ def failure_count(self) -> int:
58
+ """Return the number of failed downloads."""
59
+ return len(self.failed)
minitap/mcp/main.py CHANGED
@@ -1,7 +1,6 @@
1
1
  """MCP server for mobile-use with screen analysis capabilities."""
2
2
 
3
3
  import argparse
4
- import logging
5
4
  import os
6
5
  import sys
7
6
  import threading
@@ -23,22 +22,27 @@ if sys.platform == "win32":
23
22
 
24
23
 
25
24
  from fastmcp import FastMCP # noqa: E402
25
+ from minitap.mobile_use.config import settings as sdk_settings
26
26
 
27
27
  from minitap.mcp.core.config import settings # noqa: E402
28
- from minitap.mobile_use.config import settings as sdk_settings
29
- from minitap.mcp.core.device import (
30
- DeviceInfo, # noqa: E402
31
- list_available_devices,
28
+ from minitap.mcp.core.device import DeviceInfo # noqa: E402
29
+ from minitap.mcp.core.device import list_available_devices
30
+ from minitap.mcp.core.logging_config import (
31
+ configure_logging, # noqa: E402
32
+ get_logger,
32
33
  )
33
34
  from minitap.mcp.server.middleware import MaestroCheckerMiddleware
34
35
  from minitap.mcp.server.poller import device_health_poller
35
36
 
37
+ configure_logging(log_level=os.getenv("LOG_LEVEL", "INFO"))
38
+
36
39
 
37
40
  def main() -> None:
38
41
  """Main entry point for the MCP server."""
39
42
 
40
43
  parser = argparse.ArgumentParser(description="Mobile Use MCP Server")
41
44
  parser.add_argument("--api-key", type=str, required=False, default=None)
45
+ parser.add_argument("--llm-profile", type=str, required=False, default=None)
42
46
  parser.add_argument(
43
47
  "--server",
44
48
  action="store_true",
@@ -46,14 +50,17 @@ def main() -> None:
46
50
  )
47
51
 
48
52
  args = parser.parse_args()
49
- print("parsing args")
50
- print(args)
51
53
 
52
54
  if args.api_key:
53
55
  os.environ["MINITAP_API_KEY"] = args.api_key
54
56
  settings.__init__()
55
57
  sdk_settings.__init__()
56
58
 
59
+ if args.llm_profile:
60
+ os.environ["MINITAP_LLM_PROFILE_NAME"] = args.llm_profile
61
+ settings.__init__()
62
+ sdk_settings.__init__()
63
+
57
64
  if not settings.MINITAP_API_KEY:
58
65
  raise ValueError("Minitap API key is required to run the MCP")
59
66
 
@@ -70,7 +77,7 @@ def main() -> None:
70
77
  mcp_lifespan()
71
78
 
72
79
 
73
- logger = logging.getLogger(__name__)
80
+ logger = get_logger(__name__)
74
81
 
75
82
  mcp = FastMCP(
76
83
  name="mobile-use-mcp",
@@ -80,11 +87,10 @@ mcp = FastMCP(
80
87
  Call get_available_devices() to list them.
81
88
  """,
82
89
  )
83
-
84
- from minitap.mcp.tools import ( # noqa: E402, F401
85
- analyze_screen, # noqa: E402, F401
86
- execute_mobile_command, # noqa: E402, F401
87
- )
90
+ from minitap.mcp.tools import analyze_screen # noqa: E402, F401
91
+ from minitap.mcp.tools import compare_screenshot_with_figma # noqa: E402, F401
92
+ from minitap.mcp.tools import execute_mobile_command # noqa: E402, F401
93
+ from minitap.mcp.tools import save_figma_assets # noqa: E402, F401
88
94
 
89
95
 
90
96
  @mcp.resource("data://devices")
@@ -94,7 +100,7 @@ def get_available_devices() -> list[DeviceInfo]:
94
100
 
95
101
 
96
102
  def mcp_lifespan(**mcp_run_kwargs):
97
- from minitap.mcp.core.agents import get_mobile_use_agent # noqa: E402
103
+ from minitap.mcp.core.sdk_agent import get_mobile_use_agent # noqa: E402
98
104
 
99
105
  agent = get_mobile_use_agent()
100
106
  mcp.add_middleware(MaestroCheckerMiddleware(agent))
@@ -108,16 +114,25 @@ def mcp_lifespan(**mcp_run_kwargs):
108
114
  stop_event,
109
115
  agent,
110
116
  ),
117
+ daemon=True,
111
118
  )
112
119
  poller_thread.start()
113
120
 
114
121
  try:
115
122
  mcp.run(**mcp_run_kwargs)
116
123
  except KeyboardInterrupt:
117
- pass
118
-
119
- # Stop device health poller
120
- stop_event.set()
121
- logger.info("Device health poller stopping...")
122
- poller_thread.join()
123
- logger.info("Device health poller stopped")
124
+ logger.info("Keyboard interrupt received, shutting down...")
125
+ except Exception as e:
126
+ logger.error(f"Error running MCP server: {e}")
127
+ finally:
128
+ # Stop device health poller
129
+ logger.info("Stopping device health poller...")
130
+ stop_event.set()
131
+
132
+ # Give the poller thread a reasonable time to stop gracefully
133
+ poller_thread.join(timeout=10.0)
134
+
135
+ if poller_thread.is_alive():
136
+ logger.warning("Device health poller thread did not stop gracefully")
137
+ else:
138
+ logger.info("Device health poller stopped successfully")
@@ -1,38 +1,78 @@
1
1
  """Device health monitoring poller for the MCP server."""
2
2
 
3
+ import asyncio
3
4
  import logging
4
- import time
5
5
  import threading
6
6
 
7
- from minitap.mcp.core.device import list_available_devices
8
7
  from minitap.mobile_use.sdk import Agent
9
8
 
9
+ from minitap.mcp.core.device import list_available_devices
10
+
10
11
  logger = logging.getLogger(__name__)
11
12
 
12
13
 
13
- def device_health_poller(stop_event: threading.Event, agent: Agent) -> None:
14
+ async def _async_device_health_poller(stop_event: threading.Event, agent: Agent) -> None:
14
15
  """
15
- Background poller that monitors device availability and agent health.
16
- Runs every 5 seconds to ensure a device is connected and the agent is healthy.
16
+ Async implementation of device health poller.
17
17
 
18
18
  Args:
19
+ stop_event: Threading event to signal when to stop polling.
19
20
  agent: The Agent instance to monitor and reinitialize if needed.
20
21
  """
21
22
  while not stop_event.is_set():
22
23
  try:
23
- time.sleep(5)
24
+ # Sleep in smaller chunks to be more responsive to stop signal
25
+ for _ in range(50): # 50 * 0.1 = 5 seconds total
26
+ if stop_event.is_set():
27
+ break
28
+ await asyncio.sleep(0.1)
29
+
30
+ if stop_event.is_set():
31
+ break
24
32
 
25
33
  devices = list_available_devices()
26
34
 
27
35
  if len(devices) > 0:
28
36
  if not agent.is_healthy():
29
37
  logger.warning("Agent is not healthy. Reinitializing...")
30
- agent.clean(force=True)
31
- agent.init()
38
+ await agent.clean(force=True)
39
+ await agent.init()
32
40
  logger.info("Agent reinitialized successfully")
33
41
  else:
34
42
  logger.info("No mobile device found, retrying in 5 seconds...")
35
43
 
36
44
  except Exception as e:
37
45
  logger.error(f"Error in device health poller: {e}")
38
- agent.clean(force=True)
46
+
47
+ try:
48
+ await agent.clean(force=True)
49
+ logger.info("Agent cleaned up successfully")
50
+ except Exception as e:
51
+ logger.error(f"Error cleaning up agent: {e}")
52
+
53
+
54
+ def device_health_poller(stop_event: threading.Event, agent: Agent) -> None:
55
+ """
56
+ Background poller that monitors device availability and agent health.
57
+ Runs every 5 seconds to ensure a device is connected and the agent is healthy.
58
+
59
+ This is a sync wrapper that runs the async poller in a new event loop.
60
+
61
+ Args:
62
+ stop_event: Threading event to signal when to stop polling.
63
+ agent: The Agent instance to monitor and reinitialize if needed.
64
+ """
65
+ loop = None
66
+ try:
67
+ loop = asyncio.new_event_loop()
68
+ asyncio.set_event_loop(loop)
69
+
70
+ loop.run_until_complete(_async_device_health_poller(stop_event, agent))
71
+ except Exception as e:
72
+ logger.error(f"Error in device health poller thread: {e}")
73
+ finally:
74
+ if loop is not None:
75
+ try:
76
+ loop.close()
77
+ except Exception:
78
+ pass
@@ -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)}")
@@ -8,8 +8,8 @@ from minitap.mobile_use.sdk.types import ManualTaskConfig
8
8
  from minitap.mobile_use.sdk.types.task import PlatformTaskRequest
9
9
  from pydantic import Field
10
10
 
11
- from minitap.mcp.core.agents import get_mobile_use_agent
12
11
  from minitap.mcp.core.decorators import handle_tool_errors
12
+ from minitap.mcp.core.sdk_agent import get_mobile_use_agent
13
13
  from minitap.mcp.main import mcp
14
14
 
15
15
 
@@ -0,0 +1,282 @@
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.logging_config import get_logger
22
+ from minitap.mcp.core.models import (
23
+ AssetDownloadResult,
24
+ AssetDownloadSummary,
25
+ DownloadStatus,
26
+ FigmaDesignContextOutput,
27
+ )
28
+ from minitap.mcp.main import mcp
29
+ from minitap.mcp.tools.compare_screenshot_with_figma import (
30
+ compress_image_base64,
31
+ get_figma_screenshot,
32
+ )
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ @mcp.tool(
38
+ name="save_figma_assets",
39
+ description="""
40
+ Fetch Figma design assets/react implementation code and save them locally in the workspace.
41
+
42
+ This tool:
43
+ 1. Calls get_design_context from Figma MCP to get the React/TypeScript code
44
+ 2. Extracts all asset URLs and code implementation from the code
45
+ 3. Downloads each asset to .mobile-use/figma_assets/<node-id>/ folder
46
+ 4. Saves the code implementation to .mobile-use/figma_assets/<node-id>/code_implementation.ts
47
+ 5. Returns a list of downloaded files
48
+ """,
49
+ )
50
+ @handle_tool_errors
51
+ async def save_figma_assets(
52
+ node_id: str = Field(
53
+ description=(
54
+ "The node ID of the Figma design. Expected format is ':' separated.\n"
55
+ "Example: If given the URL https://figma.com/design/:fileKey/:fileName?node-id=1-2,\n"
56
+ "the extracted nodeId would be 1:2. Strictly respect this format."
57
+ )
58
+ ),
59
+ file_key: str = Field(
60
+ description=(
61
+ "The file key of the Figma file.\n"
62
+ "Example: If given the URL https://figma.com/design/abc123/MyFile?node-id=1-2,\n"
63
+ "the extracted fileKey would be 'abc123'."
64
+ )
65
+ ),
66
+ workspace_path: str = Field(
67
+ default=".",
68
+ description=(
69
+ "The workspace path where assets should be saved. Defaults to current directory."
70
+ ),
71
+ ),
72
+ ) -> ToolResult:
73
+ """Fetch and save Figma assets locally."""
74
+
75
+ # Step 1: Get design context from Figma MCP
76
+ design_context = await get_design_context(node_id, file_key)
77
+
78
+ # Step 2: Extract asset URLs using LLM agent
79
+ extracted_context: ExtractedAssets = await extract_figma_assets(
80
+ design_context.code_implementation
81
+ )
82
+ if not extracted_context.assets:
83
+ raise ToolError("No assets found in the Figma design context.")
84
+
85
+ # Step 3: Create directory structure
86
+ # Convert node_id format (1:2) to folder name (1-2)
87
+ folder_name = node_id.replace(":", "-")
88
+ assets_dir = Path(workspace_path) / ".mobile-use" / "figma_assets" / folder_name
89
+
90
+ # Delete existing directory to remove stale assets
91
+ if assets_dir.exists():
92
+ shutil.rmtree(assets_dir)
93
+
94
+ # Create fresh directory
95
+ assets_dir.mkdir(parents=True, exist_ok=True)
96
+
97
+ # Step 4: Download assets with resilient error handling
98
+ download_summary = AssetDownloadSummary()
99
+
100
+ for idx, asset in enumerate(extracted_context.assets, 1):
101
+ logger.debug(
102
+ "Downloading asset",
103
+ idx=idx,
104
+ total_count=len(extracted_context.assets),
105
+ variable_name=asset.variable_name,
106
+ extension=asset.extension,
107
+ )
108
+ result = download_asset(asset, assets_dir)
109
+ if result.status == DownloadStatus.SUCCESS:
110
+ logger.debug(
111
+ "Asset downloaded successfully",
112
+ idx=idx,
113
+ variable_name=asset.variable_name,
114
+ extension=asset.extension,
115
+ )
116
+ download_summary.successful.append(result)
117
+ else:
118
+ logger.debug(
119
+ "Asset download failed",
120
+ idx=idx,
121
+ variable_name=asset.variable_name,
122
+ extension=asset.extension,
123
+ error=result.error,
124
+ )
125
+ download_summary.failed.append(result)
126
+
127
+ # Step 4.5: Save code implementation
128
+ code_implementation_file = assets_dir / "code_implementation.ts"
129
+
130
+ commented_code_implementation_guidelines = ""
131
+ if design_context.code_implementation_guidelines:
132
+ commented_code_implementation_guidelines = "\n".join(
133
+ ["// " + line for line in design_context.code_implementation_guidelines.split("\n")]
134
+ )
135
+
136
+ commented_nodes_guidelines = ""
137
+ if design_context.nodes_guidelines:
138
+ commented_nodes_guidelines = "\n".join(
139
+ ["// " + line for line in design_context.nodes_guidelines.split("\n")]
140
+ )
141
+
142
+ code_implementation_file.write_text(
143
+ extracted_context.code_implementation
144
+ + "\n\n"
145
+ + commented_code_implementation_guidelines
146
+ + "\n\n"
147
+ + commented_nodes_guidelines,
148
+ encoding="utf-8",
149
+ )
150
+
151
+ # Step 5: Generate friendly output message
152
+ result_parts = []
153
+
154
+ if download_summary.successful:
155
+ result_parts.append(
156
+ f"✅ Successfully downloaded {download_summary.success_count()} asset(s) "
157
+ f"to .mobile-use/figma_assets/{folder_name}/:\n"
158
+ )
159
+ for asset_result in download_summary.successful:
160
+ result_parts.append(f" • {asset_result.filename}")
161
+
162
+ if download_summary.failed:
163
+ result_parts.append(
164
+ f"\n\n⚠️ Failed to download {download_summary.failure_count()} asset(s):"
165
+ )
166
+ for asset_result in download_summary.failed:
167
+ error_msg = f": {asset_result.error}" if asset_result.error else ""
168
+ result_parts.append(f" • {asset_result.filename}{error_msg}")
169
+
170
+ if code_implementation_file.exists():
171
+ result_parts.append(
172
+ f"\n\n✅ Successfully saved code implementation to {code_implementation_file.name}"
173
+ )
174
+
175
+ expected_screenshot = await get_figma_screenshot(node_id)
176
+ compressed_expected = compress_image_base64(expected_screenshot)
177
+
178
+ return ToolResult(
179
+ content=[
180
+ mcp_ref.types.TextContent(
181
+ type="text",
182
+ text="\n".join(result_parts),
183
+ ),
184
+ mcp_ref.types.TextContent(
185
+ type="text",
186
+ text="**Expected (Figma design)**",
187
+ ),
188
+ mcp_ref.types.ImageContent(
189
+ type="image",
190
+ data=compressed_expected,
191
+ mimeType="image/jpeg",
192
+ ),
193
+ ]
194
+ )
195
+
196
+
197
+ async def get_design_context(node_id: str, file_key: str) -> FigmaDesignContextOutput:
198
+ """Fetch design context from Figma MCP server.
199
+
200
+ Args:
201
+ node_id: The Figma node ID in format "1:2"
202
+ file_key: The Figma file key
203
+
204
+ Returns:
205
+ The React/TypeScript code as a string
206
+
207
+ Raises:
208
+ ToolError: If fetching fails
209
+ """
210
+ try:
211
+ async with Client(settings.FIGMA_MCP_SERVER_URL) as client:
212
+ result: CallToolResult = await client.call_tool(
213
+ "get_design_context",
214
+ {
215
+ "nodeId": node_id,
216
+ "fileKey": file_key,
217
+ "clientLanguages": "typescript",
218
+ "clientFrameworks": "react",
219
+ },
220
+ )
221
+
222
+ code_implementation = ""
223
+ code_implementation_guidelines = None
224
+ nodes_guidelines = None
225
+
226
+ if len(result.content) > 0 and isinstance(result.content[0], mcp_ref.types.TextContent):
227
+ code_implementation = result.content[0].text
228
+ else:
229
+ raise ToolError("Failed to fetch design context from Figma")
230
+
231
+ if len(result.content) > 1:
232
+ if isinstance(result.content[1], mcp_ref.types.TextContent):
233
+ code_implementation_guidelines = result.content[1].text
234
+ if len(result.content) > 2 and isinstance(result.content[2], mcp_ref.types.TextContent):
235
+ nodes_guidelines = result.content[2].text
236
+
237
+ return FigmaDesignContextOutput(
238
+ code_implementation=code_implementation,
239
+ code_implementation_guidelines=code_implementation_guidelines,
240
+ nodes_guidelines=nodes_guidelines,
241
+ )
242
+ except Exception as e:
243
+ raise ToolError(
244
+ f"Failed to fetch design context from Figma: {str(e)}.\n"
245
+ "Ensure the Figma MCP server is running through the official Figma desktop app."
246
+ )
247
+
248
+
249
+ def download_asset(asset: FigmaAsset, assets_dir: Path) -> AssetDownloadResult:
250
+ """Download a single asset with error handling.
251
+
252
+ Args:
253
+ asset: FigmaAsset model with variable_name, url, and extension
254
+ assets_dir: Directory to save the asset
255
+
256
+ Returns:
257
+ AssetDownloadResult with status and optional error message
258
+ """
259
+ variable_name = asset.variable_name
260
+ url = asset.url
261
+ extension = asset.extension
262
+
263
+ # Convert camelCase variable name to filename
264
+ # e.g., imgSignal -> imgSignal.svg
265
+ filename = f"{variable_name}.{extension}"
266
+ filepath = assets_dir / filename
267
+
268
+ try:
269
+ response = requests.get(url, timeout=30)
270
+ if response.status_code == 200:
271
+ filepath.write_bytes(response.content)
272
+ return AssetDownloadResult(filename=filename, status=DownloadStatus.SUCCESS)
273
+ else:
274
+ return AssetDownloadResult(
275
+ filename=filename,
276
+ status=DownloadStatus.FAILED,
277
+ error=f"HTTP {response.status_code}",
278
+ )
279
+ except requests.exceptions.Timeout:
280
+ return AssetDownloadResult(filename=filename, status=DownloadStatus.FAILED, error="Timeout")
281
+ except Exception as e:
282
+ return AssetDownloadResult(filename=filename, status=DownloadStatus.FAILED, error=str(e))
@@ -1,15 +1,17 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: minitap-mcp
3
- Version: 0.3.0
3
+ Version: 0.4.1
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>=2.6.0
10
+ Requires-Dist: minitap-mobile-use>=2.8.2
11
11
  Requires-Dist: jinja2>=3.1.6
12
12
  Requires-Dist: langchain-core>=0.3.75
13
+ Requires-Dist: pillow>=11.1.0
14
+ Requires-Dist: structlog>=24.4.0
13
15
  Requires-Dist: ruff==0.5.3 ; extra == 'dev'
14
16
  Requires-Dist: pytest==8.4.1 ; extra == 'dev'
15
17
  Requires-Dist: pytest-cov==5.0.0 ; extra == 'dev'
@@ -37,41 +39,50 @@ Before running the MCP server, ensure you have the required mobile automation to
37
39
 
38
40
  - **For Android devices:**
39
41
  - [ADB (Android Debug Bridge)](https://developer.android.com/tools/adb) - For device communication
40
- - [Maestro](https://maestro.mobile.dev/) - For mobile automation (optional but recommended)
42
+ - [Maestro](https://maestro.mobile.dev/) - For mobile automation
41
43
 
42
44
  - **For iOS devices (macOS only):**
43
45
  - Xcode Command Line Tools with `xcrun`
44
- - [Maestro](https://maestro.mobile.dev/) - For mobile automation (optional but recommended)
46
+ - [Maestro](https://maestro.mobile.dev/) - For mobile automation
45
47
 
46
48
  For detailed setup instructions, see the [mobile-use repository](https://github.com/minitap-ai/mobile-use).
47
49
 
48
- ### Configuration
50
+ ### Running the Server
49
51
 
50
- Set your Minitap API credentials as environment variables:
52
+ The simplest way to start:
51
53
 
52
54
  ```bash
53
- export MINITAP_API_KEY="your_api_key_here"
54
- export MINITAP_API_BASE_URL="https://platform.minitap.ai/api/v1"
55
- export MINITAP_LLM_PROFILE_NAME="default"
55
+ minitap-mcp --server --api-key your_minitap_api_key
56
56
  ```
57
57
 
58
- You can set these variables in your `.bashrc` or equivalent.
58
+ 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).
59
59
 
60
- ### Running the Server
60
+ **Available CLI options:**
61
+
62
+ ```bash
63
+ minitap-mcp --server --api-key YOUR_KEY --llm-profile PROFILE_NAME
64
+ ```
61
65
 
62
- **With environment variables:**
66
+ - `--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).
67
+ - `--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).
68
+
69
+ ### Configuration (Optional)
70
+
71
+ Alternatively, you can set environment variables instead of using CLI flags:
63
72
 
64
73
  ```bash
65
- minitap-mcp --server
74
+ export MINITAP_API_KEY="your_minitap_api_key"
75
+ export MINITAP_API_BASE_URL="https://platform.minitap.ai/api/v1"
76
+ export MINITAP_LLM_PROFILE_NAME="default"
66
77
  ```
67
78
 
68
- **With API key as argument:**
79
+ You can set these in your `.bashrc` or equivalent, then simply run:
69
80
 
70
81
  ```bash
71
- minitap-mcp --server --api-key your_api_key_here
82
+ minitap-mcp --server
72
83
  ```
73
84
 
74
- Using `--api-key` overrides the `MINITAP_API_KEY` environment variable, useful for quick testing.
85
+ CLI flags always override environment variables when both are present.
75
86
 
76
87
  By default, the server will bind to `0.0.0.0:8000`. Configure via environment variables:
77
88
 
@@ -82,7 +93,7 @@ export MCP_SERVER_PORT="8000"
82
93
 
83
94
  ## IDE Integration
84
95
 
85
- 1. Start the server: `minitap-mcp --server`
96
+ 1. Start the server: `minitap-mcp --server --api-key your_minitap_api_key`
86
97
  2. Add to your IDE MCP settings file:
87
98
 
88
99
  ```jsonc
@@ -118,7 +129,7 @@ Execute natural language commands on your mobile device using the Minitap SDK. T
118
129
 
119
130
  **Parameters:**
120
131
  - `goal` (required): High-level goal describing the action to perform
121
- - `output_description` (optional): Description of expected output format
132
+ - `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")
122
133
  - `profile` (optional): Profile name to use (defaults to "default")
123
134
 
124
135
  **Examples:**
@@ -0,0 +1,26 @@
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=4XwPi7JRGPPvW9P4igQFdKYG94cM5Y8qc3pWRkQ8BsU,3095
6
+ minitap/mcp/core/config.py,sha256=_rIH31treZlM2RVnTz5cPXhV9Bu4D-w4TmbPh5_mxxM,1026
7
+ minitap/mcp/core/decorators.py,sha256=kMx_mlaa-2U1AgCoYkgPoLOa-iOoKUF1OjcNV7x59Ds,2940
8
+ minitap/mcp/core/device.py,sha256=sEO3Z-8F325hDOObdH1YBhZE60f17FmIclt5UlhY_nU,7875
9
+ minitap/mcp/core/llm.py,sha256=tI5m5rFDLeMkXE5WExnzYSzHU3nTIEiSC9nAsPzVMaU,1144
10
+ minitap/mcp/core/logging_config.py,sha256=OJlArPJxflbhckerFsRHVTzy3jwsLsNSPN0LVpkmpNM,1861
11
+ minitap/mcp/core/models.py,sha256=egLScxPAMo4u5cqY33UKba7z7DsdgqfPW409UAqW1Jg,1942
12
+ minitap/mcp/core/sdk_agent.py,sha256=-9l1YetD93dzxOeSFOT_j8dDfDFjhJLiir8bhzEjI3Y,900
13
+ minitap/mcp/core/utils.py,sha256=3uExpRoh7affIieZx3TLlZTmZCcoxWfx1YpPbwhjiJY,1791
14
+ minitap/mcp/main.py,sha256=VBjwrZUoCGlddJaJM-17j180YXvnUPnd5IgR_ckheZs,4481
15
+ minitap/mcp/server/middleware.py,sha256=fbry_IiHmwUxVjsWgOU2goybcS1kLRXFZZ89KPH1d8E,880
16
+ minitap/mcp/server/poller.py,sha256=Qakq4yO3EJ9dXmRqtE3sJjyk0ij7VBU-NuupHhTf37g,2539
17
+ minitap/mcp/tools/analyze_screen.py,sha256=fjcjf3tTZDlxzmiQFHFNgw38bxPz4eisw57zuxshN2A,1984
18
+ minitap/mcp/tools/compare_screenshot_with_figma.py,sha256=G69F6vRFI2tE2wW-oFYPjnY8oFMD9nRZH0H-yvtD4gE,4575
19
+ minitap/mcp/tools/execute_mobile_command.py,sha256=qY3UfcDq1BtYcny1YlEF4WV9LwUJxLAmLJCm1VBzxS8,2442
20
+ minitap/mcp/tools/go_back.py,sha256=lEmADkDkXu8JGm-sY7zL7M6GlBy-lD7Iffv4yzwoQfo,1301
21
+ minitap/mcp/tools/save_figma_assets.py,sha256=T5a_7wi1aLoyCrn4FWwXEE_m8dXqUu2ZFGDKst_sKHI,9985
22
+ minitap/mcp/tools/screen_analyzer.md,sha256=TTO80JQWusbA9cKAZn-9cqhgVHm6F_qJh5w152hG3YM,734
23
+ minitap_mcp-0.4.1.dist-info/WHEEL,sha256=5w2T7AS2mz1-rW9CNagNYWRCaB0iQqBMYLwKdlgiR4Q,78
24
+ minitap_mcp-0.4.1.dist-info/entry_points.txt,sha256=rYVoXm7tSQCqQTtHx4Lovgn1YsjwtEEHfddKrfEVHuY,55
25
+ minitap_mcp-0.4.1.dist-info/METADATA,sha256=dmXqGtUSR1-KPIXxT-DLHyMWrHp5mzMzP40Bo-BmmDU,5918
26
+ minitap_mcp-0.4.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.4
2
+ Generator: uv 0.9.7
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,18 +0,0 @@
1
- minitap/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- minitap/mcp/core/agents.py,sha256=-9l1YetD93dzxOeSFOT_j8dDfDFjhJLiir8bhzEjI3Y,900
3
- minitap/mcp/core/config.py,sha256=ohJ50qQp6wKFBcX8hr0IbSGnrlbWg1YAwrgJ9I1_vLE,849
4
- minitap/mcp/core/decorators.py,sha256=iekv181o_rkv0upacFWkmPqxsZRTzuLFyOZ0sIDtQnQ,1317
5
- minitap/mcp/core/device.py,sha256=sEO3Z-8F325hDOObdH1YBhZE60f17FmIclt5UlhY_nU,7875
6
- minitap/mcp/core/llm.py,sha256=z_pYZkZcAchsiWPh4W79frQPANsfYyFPUe8DJo8lZO0,822
7
- minitap/mcp/core/utils.py,sha256=3uExpRoh7affIieZx3TLlZTmZCcoxWfx1YpPbwhjiJY,1791
8
- minitap/mcp/main.py,sha256=eoF-wDgXmS_eZYpWBJBXTQaOOGhHHxY-IIwG0C6cW9s,3557
9
- minitap/mcp/server/middleware.py,sha256=fbry_IiHmwUxVjsWgOU2goybcS1kLRXFZZ89KPH1d8E,880
10
- minitap/mcp/server/poller.py,sha256=C2h5Ir3nY5gZ6qTDOHBw_Tb8PfAY54A-we2HrwjNLvg,1222
11
- minitap/mcp/tools/analyze_screen.py,sha256=fjcjf3tTZDlxzmiQFHFNgw38bxPz4eisw57zuxshN2A,1984
12
- minitap/mcp/tools/execute_mobile_command.py,sha256=f5yObnn9r2pZ33w0I2TwvCZKKepqlKbpVANZnUkfFjU,2439
13
- minitap/mcp/tools/go_back.py,sha256=lEmADkDkXu8JGm-sY7zL7M6GlBy-lD7Iffv4yzwoQfo,1301
14
- minitap/mcp/tools/screen_analyzer.md,sha256=TTO80JQWusbA9cKAZn-9cqhgVHm6F_qJh5w152hG3YM,734
15
- minitap_mcp-0.3.0.dist-info/WHEEL,sha256=k57ZwB-NkeM_6AsPnuOHv5gI5KM5kPD6Vx85WmGEcI0,78
16
- minitap_mcp-0.3.0.dist-info/entry_points.txt,sha256=rYVoXm7tSQCqQTtHx4Lovgn1YsjwtEEHfddKrfEVHuY,55
17
- minitap_mcp-0.3.0.dist-info/METADATA,sha256=5BB82VEtjp_R5mcyxD40DspuYXdgEOijlFf9nFqr3NM,5128
18
- minitap_mcp-0.3.0.dist-info/RECORD,,
File without changes