minitap-mcp 0.4.0__tar.gz → 0.4.2__tar.gz

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.
Files changed (26) hide show
  1. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/PKG-INFO +3 -2
  2. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/agents/extract_figma_assets.py +37 -6
  3. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/config.py +1 -0
  4. minitap_mcp-0.4.2/minitap/mcp/core/decorators.py +89 -0
  5. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/llm.py +11 -0
  6. minitap_mcp-0.4.2/minitap/mcp/core/logging_config.py +59 -0
  7. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/main.py +19 -2
  8. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/tools/save_figma_assets.py +26 -2
  9. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/pyproject.toml +3 -2
  10. minitap_mcp-0.4.0/minitap/mcp/core/decorators.py +0 -42
  11. minitap_mcp-0.4.0/minitap/mcp/tools/go_back.py +0 -42
  12. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/PYPI_README.md +0 -0
  13. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/__init__.py +0 -0
  14. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/agents/compare_screenshots.md +0 -0
  15. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/agents/compare_screenshots.py +0 -0
  16. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/agents/extract_figma_assets.md +0 -0
  17. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/device.py +0 -0
  18. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/models.py +0 -0
  19. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/sdk_agent.py +0 -0
  20. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/core/utils.py +0 -0
  21. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/server/middleware.py +0 -0
  22. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/server/poller.py +0 -0
  23. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/tools/analyze_screen.py +0 -0
  24. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/tools/compare_screenshot_with_figma.py +0 -0
  25. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/tools/execute_mobile_command.py +0 -0
  26. {minitap_mcp-0.4.0 → minitap_mcp-0.4.2}/minitap/mcp/tools/screen_analyzer.md +0 -0
@@ -1,16 +1,17 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: minitap-mcp
3
- Version: 0.4.0
3
+ Version: 0.4.2
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.8.1
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
13
  Requires-Dist: pillow>=11.1.0
14
+ Requires-Dist: structlog>=24.4.0
14
15
  Requires-Dist: ruff==0.5.3 ; extra == 'dev'
15
16
  Requires-Dist: pytest==8.4.1 ; extra == 'dev'
16
17
  Requires-Dist: pytest-cov==5.0.0 ; extra == 'dev'
@@ -1,7 +1,8 @@
1
1
  """Agent to extract Figma asset URLs from design context code."""
2
2
 
3
+ import re
4
+ import uuid
3
5
  from pathlib import Path
4
- from uuid import uuid4
5
6
 
6
7
  from jinja2 import Template
7
8
  from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
@@ -33,6 +34,35 @@ class ExtractedAssets(BaseModel):
33
34
  )
34
35
 
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
+
36
66
  async def extract_figma_assets(design_context_code: str) -> ExtractedAssets:
37
67
  """Extract asset URLs from Figma design context code.
38
68
 
@@ -46,20 +76,21 @@ async def extract_figma_assets(design_context_code: str) -> ExtractedAssets:
46
76
  Path(__file__).parent.joinpath("extract_figma_assets.md").read_text(encoding="utf-8")
47
77
  ).render()
48
78
 
79
+ sanitized_code = sanitize_unicode_for_llm(design_context_code)
80
+
49
81
  messages: list[BaseMessage] = [
50
82
  SystemMessage(content=system_message),
51
83
  HumanMessage(
52
- content=f"Here is the code to analyze:\n\n```typescript\n{design_context_code}\n```"
84
+ content=f"Here is the code to analyze:\n\n```typescript\n{sanitized_code}\n```"
53
85
  ),
54
86
  ]
55
87
 
56
88
  llm = get_minitap_llm(
57
- trace_id=str(uuid4()),
58
- remote_tracing=True,
59
- model="google/gemini-2.5-pro",
89
+ model="openai/gpt-5",
60
90
  temperature=0,
91
+ trace_id=str(uuid.uuid4()),
92
+ remote_tracing=True,
61
93
  ).with_structured_output(ExtractedAssets)
62
-
63
94
  result: ExtractedAssets = await llm.ainvoke(messages) # type: ignore
64
95
 
65
96
  return result
@@ -16,6 +16,7 @@ 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
 
@@ -0,0 +1,89 @@
1
+ """Decorators for MCP tools."""
2
+
3
+ import inspect
4
+ import traceback
5
+ from collections.abc import Callable
6
+ from functools import wraps
7
+ from typing import Any, TypeVar
8
+
9
+ from minitap.mcp.core.device import DeviceNotFoundError
10
+ from minitap.mcp.core.logging_config import get_logger
11
+
12
+ F = TypeVar("F", bound=Callable[..., Any])
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ def handle_tool_errors[T: Callable[..., Any]](func: T) -> T:
18
+ """
19
+ Decorator that catches all exceptions in MCP tools and returns error messages.
20
+
21
+ This prevents unhandled exceptions from causing infinite loops in the MCP server.
22
+ Logs all errors with structured logging for better debugging.
23
+ """
24
+
25
+ @wraps(func)
26
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
27
+ try:
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
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
+ )
44
+ return f"Error: {str(e)}"
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
+ )
53
+ return f"Error in {func.__name__}: {type(e).__name__}: {str(e)}"
54
+
55
+ @wraps(func)
56
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
57
+ try:
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
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
+ )
74
+ return f"Error: {str(e)}"
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
+ )
83
+ return f"Error in {func.__name__}: {type(e).__name__}: {str(e)}"
84
+
85
+ # Check if the function is async
86
+ if inspect.iscoroutinefunction(func):
87
+ return async_wrapper # type: ignore
88
+ else:
89
+ return sync_wrapper # type: ignore
@@ -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)
@@ -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
@@ -28,9 +27,15 @@ from minitap.mobile_use.config import settings as sdk_settings
28
27
  from minitap.mcp.core.config import settings # noqa: E402
29
28
  from minitap.mcp.core.device import DeviceInfo # noqa: E402
30
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,
33
+ )
31
34
  from minitap.mcp.server.middleware import MaestroCheckerMiddleware
32
35
  from minitap.mcp.server.poller import device_health_poller
33
36
 
37
+ configure_logging(log_level=os.getenv("LOG_LEVEL", "INFO"))
38
+
34
39
 
35
40
  def main() -> None:
36
41
  """Main entry point for the MCP server."""
@@ -43,6 +48,13 @@ def main() -> None:
43
48
  action="store_true",
44
49
  help="Run as network server (uses MCP_SERVER_HOST and MCP_SERVER_PORT from env)",
45
50
  )
51
+ parser.add_argument(
52
+ "--port",
53
+ type=int,
54
+ required=False,
55
+ default=None,
56
+ help="Port to run the server on (overrides MCP_SERVER_PORT env variable)",
57
+ )
46
58
 
47
59
  args = parser.parse_args()
48
60
 
@@ -56,6 +68,11 @@ def main() -> None:
56
68
  settings.__init__()
57
69
  sdk_settings.__init__()
58
70
 
71
+ if args.port:
72
+ os.environ["MCP_SERVER_PORT"] = str(args.port)
73
+ settings.__init__()
74
+ sdk_settings.__init__()
75
+
59
76
  if not settings.MINITAP_API_KEY:
60
77
  raise ValueError("Minitap API key is required to run the MCP")
61
78
 
@@ -72,7 +89,7 @@ def main() -> None:
72
89
  mcp_lifespan()
73
90
 
74
91
 
75
- logger = logging.getLogger(__name__)
92
+ logger = get_logger(__name__)
76
93
 
77
94
  mcp = FastMCP(
78
95
  name="mobile-use-mcp",
@@ -18,6 +18,7 @@ from minitap.mcp.core.agents.extract_figma_assets import (
18
18
  )
19
19
  from minitap.mcp.core.config import settings
20
20
  from minitap.mcp.core.decorators import handle_tool_errors
21
+ from minitap.mcp.core.logging_config import get_logger
21
22
  from minitap.mcp.core.models import (
22
23
  AssetDownloadResult,
23
24
  AssetDownloadSummary,
@@ -30,6 +31,8 @@ from minitap.mcp.tools.compare_screenshot_with_figma import (
30
31
  get_figma_screenshot,
31
32
  )
32
33
 
34
+ logger = get_logger(__name__)
35
+
33
36
 
34
37
  @mcp.tool(
35
38
  name="save_figma_assets",
@@ -94,11 +97,31 @@ async def save_figma_assets(
94
97
  # Step 4: Download assets with resilient error handling
95
98
  download_summary = AssetDownloadSummary()
96
99
 
97
- for asset in extracted_context.assets:
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
+ )
98
108
  result = download_asset(asset, assets_dir)
99
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
+ )
100
116
  download_summary.successful.append(result)
101
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
+ )
102
125
  download_summary.failed.append(result)
103
126
 
104
127
  # Step 4.5: Save code implementation
@@ -121,7 +144,8 @@ async def save_figma_assets(
121
144
  + "\n\n"
122
145
  + commented_code_implementation_guidelines
123
146
  + "\n\n"
124
- + commented_nodes_guidelines
147
+ + commented_nodes_guidelines,
148
+ encoding="utf-8",
125
149
  )
126
150
 
127
151
  # Step 5: Generate friendly output message
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "minitap-mcp"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  description = "Model Context Protocol server for controlling Android & iOS devices with natural language"
5
5
  readme = "PYPI_README.md"
6
6
 
@@ -17,10 +17,11 @@ dependencies = [
17
17
  "python-dotenv>=1.1.1",
18
18
  "pydantic>=2.12.0",
19
19
  "pydantic-settings>=2.10.1",
20
- "minitap-mobile-use>=2.8.1",
20
+ "minitap-mobile-use>=2.8.2",
21
21
  "jinja2>=3.1.6",
22
22
  "langchain-core>=0.3.75",
23
23
  "pillow>=11.1.0",
24
+ "structlog>=24.4.0",
24
25
  ]
25
26
 
26
27
  [project.optional-dependencies]
@@ -1,42 +0,0 @@
1
- """Decorators for MCP tools."""
2
-
3
- import inspect
4
- from collections.abc import Callable
5
- from functools import wraps
6
- from typing import Any, TypeVar
7
-
8
- from minitap.mcp.core.device import DeviceNotFoundError
9
-
10
- F = TypeVar("F", bound=Callable[..., Any])
11
-
12
-
13
- def handle_tool_errors[T: Callable[..., Any]](func: T) -> T:
14
- """
15
- Decorator that catches all exceptions in MCP tools and returns error messages.
16
-
17
- This prevents unhandled exceptions from causing infinite loops in the MCP server.
18
- """
19
-
20
- @wraps(func)
21
- async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
22
- try:
23
- return await func(*args, **kwargs)
24
- except DeviceNotFoundError as e:
25
- return f"Error: {str(e)}"
26
- except Exception as e:
27
- return f"Error in {func.__name__}: {type(e).__name__}: {str(e)}"
28
-
29
- @wraps(func)
30
- def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
31
- try:
32
- return func(*args, **kwargs)
33
- except DeviceNotFoundError as e:
34
- return f"Error: {str(e)}"
35
- except Exception as e:
36
- return f"Error in {func.__name__}: {type(e).__name__}: {str(e)}"
37
-
38
- # Check if the function is async
39
- if inspect.iscoroutinefunction(func):
40
- return async_wrapper # type: ignore
41
- else:
42
- return sync_wrapper # type: ignore
@@ -1,42 +0,0 @@
1
- import requests
2
-
3
- from minitap.mcp.core.decorators import handle_tool_errors
4
- from minitap.mcp.main import mcp
5
-
6
-
7
- @mcp.tool(
8
- name="go_back",
9
- tags={"requires-maestro"},
10
- description="""
11
- Sends a 'back' command to the mobile device automation server.
12
- """,
13
- )
14
- @handle_tool_errors
15
- async def go_back() -> str:
16
- """Send a back command to the mobile device."""
17
- try:
18
- response = requests.post(
19
- "http://localhost:9999/api/run-command",
20
- headers={
21
- "User-Agent": "python-requests/2.32.4",
22
- "Accept-Encoding": "gzip, deflate, zstd",
23
- "Accept": "*/*",
24
- "Connection": "keep-alive",
25
- "Content-Type": "application/json",
26
- },
27
- json={"yaml": "back\n"},
28
- timeout=30,
29
- )
30
-
31
- if response.status_code == 200:
32
- return f"Successfully sent back command. Response: {response.text}"
33
- else:
34
- return (
35
- f"Failed to send back command. "
36
- f"Status code: {response.status_code}, Response: {response.text}"
37
- )
38
-
39
- except requests.exceptions.RequestException as e:
40
- return f"Error sending back command: {str(e)}"
41
- except Exception as e:
42
- return f"Unexpected error: {str(e)}"
File without changes