minitap-mcp 0.4.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.
@@ -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
 
@@ -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)
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
@@ -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."""
@@ -72,7 +77,7 @@ def main() -> None:
72
77
  mcp_lifespan()
73
78
 
74
79
 
75
- logger = logging.getLogger(__name__)
80
+ logger = get_logger(__name__)
76
81
 
77
82
  mcp = FastMCP(
78
83
  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,16 +1,17 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: minitap-mcp
3
- Version: 0.4.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.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'
@@ -2,24 +2,25 @@ minitap/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  minitap/mcp/core/agents/compare_screenshots.md,sha256=Gt27HVzXzu71BxcanKPokz1dFPvq90vXbjE2HOn5X0I,3559
3
3
  minitap/mcp/core/agents/compare_screenshots.py,sha256=Yb7kR8Cv0gWzXyNf-6IS7_9l1npfqYmL-SONJJGgzM4,2060
4
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=WAmn4CvN1ONJkJp2KH9l080hhZ_ge0Pdan6ejk_GWOo,2038
6
- minitap/mcp/core/config.py,sha256=gfx-cXJsgB_W2dSNHnb5jeWEYfe3VBZUMeF1nbNAdiQ,962
7
- minitap/mcp/core/decorators.py,sha256=iekv181o_rkv0upacFWkmPqxsZRTzuLFyOZ0sIDtQnQ,1317
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
8
  minitap/mcp/core/device.py,sha256=sEO3Z-8F325hDOObdH1YBhZE60f17FmIclt5UlhY_nU,7875
9
- minitap/mcp/core/llm.py,sha256=z_pYZkZcAchsiWPh4W79frQPANsfYyFPUe8DJo8lZO0,822
9
+ minitap/mcp/core/llm.py,sha256=tI5m5rFDLeMkXE5WExnzYSzHU3nTIEiSC9nAsPzVMaU,1144
10
+ minitap/mcp/core/logging_config.py,sha256=OJlArPJxflbhckerFsRHVTzy3jwsLsNSPN0LVpkmpNM,1861
10
11
  minitap/mcp/core/models.py,sha256=egLScxPAMo4u5cqY33UKba7z7DsdgqfPW409UAqW1Jg,1942
11
12
  minitap/mcp/core/sdk_agent.py,sha256=-9l1YetD93dzxOeSFOT_j8dDfDFjhJLiir8bhzEjI3Y,900
12
13
  minitap/mcp/core/utils.py,sha256=3uExpRoh7affIieZx3TLlZTmZCcoxWfx1YpPbwhjiJY,1791
13
- minitap/mcp/main.py,sha256=B7KE6_5UNGKanS0WMJYBq8vp0HE_Lr0BG9KR4BwYxwU,4341
14
+ minitap/mcp/main.py,sha256=VBjwrZUoCGlddJaJM-17j180YXvnUPnd5IgR_ckheZs,4481
14
15
  minitap/mcp/server/middleware.py,sha256=fbry_IiHmwUxVjsWgOU2goybcS1kLRXFZZ89KPH1d8E,880
15
16
  minitap/mcp/server/poller.py,sha256=Qakq4yO3EJ9dXmRqtE3sJjyk0ij7VBU-NuupHhTf37g,2539
16
17
  minitap/mcp/tools/analyze_screen.py,sha256=fjcjf3tTZDlxzmiQFHFNgw38bxPz4eisw57zuxshN2A,1984
17
18
  minitap/mcp/tools/compare_screenshot_with_figma.py,sha256=G69F6vRFI2tE2wW-oFYPjnY8oFMD9nRZH0H-yvtD4gE,4575
18
19
  minitap/mcp/tools/execute_mobile_command.py,sha256=qY3UfcDq1BtYcny1YlEF4WV9LwUJxLAmLJCm1VBzxS8,2442
19
20
  minitap/mcp/tools/go_back.py,sha256=lEmADkDkXu8JGm-sY7zL7M6GlBy-lD7Iffv4yzwoQfo,1301
20
- minitap/mcp/tools/save_figma_assets.py,sha256=EN0u0TkCUXoz8guehxm-CywKYYmZFg_d4x35eTNAovQ,9182
21
+ minitap/mcp/tools/save_figma_assets.py,sha256=T5a_7wi1aLoyCrn4FWwXEE_m8dXqUu2ZFGDKst_sKHI,9985
21
22
  minitap/mcp/tools/screen_analyzer.md,sha256=TTO80JQWusbA9cKAZn-9cqhgVHm6F_qJh5w152hG3YM,734
22
- minitap_mcp-0.4.0.dist-info/WHEEL,sha256=5w2T7AS2mz1-rW9CNagNYWRCaB0iQqBMYLwKdlgiR4Q,78
23
- minitap_mcp-0.4.0.dist-info/entry_points.txt,sha256=rYVoXm7tSQCqQTtHx4Lovgn1YsjwtEEHfddKrfEVHuY,55
24
- minitap_mcp-0.4.0.dist-info/METADATA,sha256=27wi_Bedtm971es6fHljAF8wE45uCkSoYYwwqqFvmY0,5885
25
- minitap_mcp-0.4.0.dist-info/RECORD,,
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,,