minitap-mcp 0.8.1__py3-none-any.whl → 0.9.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.
@@ -32,7 +32,15 @@ async def upload_apk_to_cloud_mobile(apk_path: str) -> str:
32
32
  if not apk_file.exists():
33
33
  raise FileNotFoundError(f"APK file not found: {apk_path}")
34
34
 
35
- filename = f"app_{uuid.uuid4().hex[:6]}.apk"
35
+ # Use APP_PACKAGE_NAME env var if set, otherwise generate a random name
36
+ # Preserve the original file extension
37
+ extension = apk_file.suffix # e.g., ".apk"
38
+ if settings.APP_PACKAGE_NAME:
39
+ # Strip any existing extension from APP_PACKAGE_NAME to avoid double extensions
40
+ base_name = Path(settings.APP_PACKAGE_NAME).stem
41
+ filename = f"{base_name}{extension}"
42
+ else:
43
+ filename = f"app_{uuid.uuid4().hex[:6]}{extension}"
36
44
  api_key = settings.MINITAP_API_KEY.get_secret_value()
37
45
  api_base_url = settings.MINITAP_API_BASE_URL.rstrip("/")
38
46
 
@@ -1,5 +1,6 @@
1
1
  """Configuration for the MCP server."""
2
2
 
3
+ import os
3
4
  from urllib.parse import urlparse
4
5
 
5
6
  from dotenv import load_dotenv
@@ -27,7 +28,17 @@ def _derive_daas_url_from_base(base_url: str) -> str:
27
28
  Example: https://dev.platform.minitap.ai/api/v1 -> https://dev.platform.minitap.ai/api/daas/
28
29
  """
29
30
  parsed = urlparse(base_url)
30
- return f"{parsed.scheme}://{parsed.netloc}/api/daas/"
31
+ return f"{parsed.scheme}://{parsed.netloc}/api/daas"
32
+
33
+
34
+ def _derive_platform_base_url(api_base_url: str) -> str:
35
+ """Derive the platform base URL from the API base URL.
36
+
37
+ Extracts the scheme and host from the API URL (strips /api/v1 path).
38
+ Example: https://dev.platform.minitap.ai/api/v1 -> https://dev.platform.minitap.ai
39
+ """
40
+ parsed = urlparse(api_base_url)
41
+ return f"{parsed.scheme}://{parsed.netloc}"
31
42
 
32
43
 
33
44
  class MCPSettings(BaseSettings):
@@ -57,6 +68,16 @@ class MCPSettings(BaseSettings):
57
68
  # Create cloud mobiles at https://platform.minitap.ai/cloud-mobiles
58
69
  CLOUD_MOBILE_NAME: str | None = Field(default=None)
59
70
 
71
+ # Trajectory GIF download configuration
72
+ # When set, downloads the trajectory GIF after task execution to the specified folder.
73
+ # The folder is a directory where the GIF will be saved with the task run ID as filename.
74
+ TRAJECTORY_GIF_DOWNLOAD_FOLDER: str | None = Field(default=None)
75
+
76
+ # App package name override for uploads
77
+ # When set, uploaded APK/IPA files will use this name instead of a random UUID.
78
+ # The original file extension is preserved. Example: "my_app" -> "my_app.apk"
79
+ APP_PACKAGE_NAME: str | None = Field(default=None)
80
+
60
81
  @model_validator(mode="after")
61
82
  def derive_urls_from_base(self) -> "MCPSettings":
62
83
  """Derive MCP and DaaS URLs from base URL if not explicitly set.
@@ -79,6 +100,11 @@ class MCPSettings(BaseSettings):
79
100
  _derive_daas_url_from_base(self.MINITAP_API_BASE_URL),
80
101
  )
81
102
 
103
+ # Set MINITAP_BASE_URL in environment for mobile-use SDK compatibility.
104
+ # The SDK uses MINITAP_BASE_URL (e.g., https://dev.platform.minitap.ai) while
105
+ # MCP uses MINITAP_API_BASE_URL (e.g., https://dev.platform.minitap.ai/api/v1).
106
+ os.environ["MINITAP_BASE_URL"] = _derive_platform_base_url(self.MINITAP_API_BASE_URL)
107
+
82
108
  return self
83
109
 
84
110
 
@@ -13,9 +13,11 @@ def configure_logging(log_level: str = "INFO") -> None:
13
13
  log_level: The logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
14
14
  """
15
15
  # Configure standard library logging
16
+ # CRITICAL: Use stderr to avoid polluting stdout with non-JSONRPC data
17
+ # MCP servers communicate via stdio - stdout MUST only contain JSONRPC messages
16
18
  logging.basicConfig(
17
19
  format="%(message)s",
18
- stream=sys.stdout,
20
+ stream=sys.stderr,
19
21
  level=getattr(logging, log_level.upper()),
20
22
  )
21
23
 
@@ -272,3 +272,136 @@ def _guess_mime_type(extension: str) -> str:
272
272
  "txt": "text/plain",
273
273
  }
274
274
  return ext_to_mime.get(ext, "application/octet-stream")
275
+
276
+
277
+ class StorageDownloadError(Exception):
278
+ """Error raised when file download fails."""
279
+
280
+ pass
281
+
282
+
283
+ async def get_trajectory_gif_download_url(task_run_id: str) -> str:
284
+ """Get a signed download URL for a trajectory GIF.
285
+
286
+ This function calls the MaaS API to get a signed S3 download URL for the
287
+ trajectory GIF associated with a task run.
288
+
289
+ Args:
290
+ task_run_id: The ID of the task run to get the GIF for
291
+
292
+ Returns:
293
+ The signed download URL for the GIF
294
+
295
+ Raises:
296
+ StorageDownloadError: If the request fails or no URL is returned
297
+ """
298
+ try:
299
+ api_key = _get_api_key()
300
+ except StorageUploadError as e:
301
+ raise StorageDownloadError(str(e)) from e
302
+ base_url = settings.MINITAP_API_BASE_URL
303
+ endpoint = f"{base_url}/storage/trajectory-gif-download/{task_run_id}"
304
+
305
+ async with httpx.AsyncClient(timeout=30.0) as client:
306
+ try:
307
+ logger.debug("Requesting trajectory GIF download URL", task_run_id=task_run_id)
308
+ response = await client.get(
309
+ endpoint,
310
+ headers={"Authorization": f"Bearer {api_key}"},
311
+ )
312
+
313
+ if response.status_code == 404:
314
+ raise StorageDownloadError(f"Trajectory GIF not found for task run: {task_run_id}")
315
+
316
+ if response.status_code != 200:
317
+ logger.error(
318
+ "Failed to get trajectory GIF download URL",
319
+ status_code=response.status_code,
320
+ response=response.text,
321
+ )
322
+ raise StorageDownloadError(
323
+ f"Failed to get trajectory GIF download URL: HTTP {response.status_code}"
324
+ )
325
+
326
+ data = response.json()
327
+ download_url = data.get("signed_url")
328
+ if not download_url:
329
+ raise StorageDownloadError("No download URL returned in response")
330
+
331
+ logger.debug("Got trajectory GIF download URL", task_run_id=task_run_id)
332
+ return download_url
333
+
334
+ except httpx.TimeoutException as e:
335
+ logger.error("Trajectory GIF download URL request timed out", error=str(e))
336
+ raise StorageDownloadError("Request timed out") from e
337
+ except httpx.RequestError as e:
338
+ logger.error("Trajectory GIF download URL request failed", error=str(e))
339
+ raise StorageDownloadError(f"Request failed: {str(e)}") from e
340
+ except StorageDownloadError:
341
+ raise
342
+ except Exception as e:
343
+ logger.error("Unexpected error getting trajectory GIF download URL", error=str(e))
344
+ raise StorageDownloadError(f"Unexpected error: {str(e)}") from e
345
+
346
+
347
+ async def download_trajectory_gif(task_run_id: str, download_path: str | Path) -> Path:
348
+ """Download a trajectory GIF to a local path.
349
+
350
+ This function:
351
+ 1. Gets a signed download URL from the MaaS API
352
+ 2. Downloads the GIF from that URL
353
+ 3. Saves it to the specified local path
354
+
355
+ Args:
356
+ task_run_id: The ID of the task run to download the GIF for
357
+ download_path: Directory path where the GIF will be saved.
358
+ The file will be saved as {task_run_id}/trajectory.gif
359
+
360
+ Returns:
361
+ The full path to the downloaded GIF file
362
+
363
+ Raises:
364
+ StorageDownloadError: If download fails at any step
365
+ """
366
+ download_dir = Path(download_path) / task_run_id
367
+
368
+ try:
369
+ download_dir.mkdir(parents=True, exist_ok=True)
370
+ except OSError as e:
371
+ raise StorageDownloadError(f"Failed to create download directory: {e}") from e
372
+
373
+ download_url = await get_trajectory_gif_download_url(task_run_id)
374
+
375
+ output_file = download_dir / "trajectory.gif"
376
+
377
+ async with httpx.AsyncClient(timeout=60.0) as client:
378
+ try:
379
+ logger.info(
380
+ "Downloading trajectory GIF", task_run_id=task_run_id, path=str(output_file)
381
+ )
382
+ response = await client.get(download_url)
383
+
384
+ if response.status_code != 200:
385
+ raise StorageDownloadError(f"Failed to download GIF: HTTP {response.status_code}")
386
+
387
+ output_file.write_bytes(response.content)
388
+ logger.info(
389
+ "Trajectory GIF downloaded successfully",
390
+ task_run_id=task_run_id,
391
+ path=str(output_file),
392
+ size=len(response.content),
393
+ )
394
+
395
+ return output_file
396
+
397
+ except httpx.TimeoutException as e:
398
+ logger.error("GIF download timed out", error=str(e))
399
+ raise StorageDownloadError("Download timed out") from e
400
+ except httpx.RequestError as e:
401
+ logger.error("GIF download request failed", error=str(e))
402
+ raise StorageDownloadError(f"Download failed: {str(e)}") from e
403
+ except StorageDownloadError:
404
+ raise
405
+ except Exception as e:
406
+ logger.error("Unexpected error downloading GIF", error=str(e))
407
+ raise StorageDownloadError(f"Unexpected error: {str(e)}") from e
@@ -0,0 +1,100 @@
1
+ """Task runs API utilities.
2
+
3
+ This module provides functionality to interact with the task runs API
4
+ for fetching information about executed tasks.
5
+ """
6
+
7
+ import httpx
8
+
9
+ from minitap.mcp.core.config import settings
10
+ from minitap.mcp.core.logging_config import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class TaskRunsError(Exception):
16
+ """Error raised when task runs API operations fail."""
17
+
18
+ pass
19
+
20
+
21
+ def _get_api_key() -> str:
22
+ """Get the API key from settings.
23
+
24
+ Returns:
25
+ The API key string
26
+
27
+ Raises:
28
+ TaskRunsError: If API key is not configured
29
+ """
30
+ api_key = settings.MINITAP_API_KEY.get_secret_value() if settings.MINITAP_API_KEY else None
31
+ if not api_key:
32
+ raise TaskRunsError("MINITAP_API_KEY is required for task runs API")
33
+ return api_key
34
+
35
+
36
+ async def get_latest_task_run_id() -> str:
37
+ """Get the ID of the most recently finished task run.
38
+
39
+ This function calls the MaaS API to get the latest task run,
40
+ sorted by finished_at in descending order.
41
+
42
+ Returns:
43
+ The ID of the latest task run
44
+
45
+ Raises:
46
+ TaskRunsError: If the request fails or no task run is found
47
+ """
48
+ api_key = _get_api_key()
49
+ base_url = settings.MINITAP_API_BASE_URL
50
+ endpoint = f"{base_url}/task-runs"
51
+
52
+ params = {
53
+ "page": 1,
54
+ "pageSize": 1,
55
+ "orphans": "include",
56
+ "virtualMobile": "include",
57
+ "sortBy": "finished_at",
58
+ "sortOrder": "desc",
59
+ }
60
+
61
+ async with httpx.AsyncClient(timeout=30.0) as client:
62
+ try:
63
+ logger.debug("Fetching latest task run ID")
64
+ response = await client.get(
65
+ endpoint,
66
+ params=params,
67
+ headers={"Authorization": f"Bearer {api_key}"},
68
+ )
69
+
70
+ if response.status_code != 200:
71
+ logger.error(
72
+ "Failed to get latest task run",
73
+ status_code=response.status_code,
74
+ response=response.text,
75
+ )
76
+ raise TaskRunsError(f"Failed to get latest task run: HTTP {response.status_code}")
77
+
78
+ data = response.json()
79
+ items = data.get("runs", [])
80
+ if not items:
81
+ raise TaskRunsError("No task runs found")
82
+
83
+ task_run_id = items[0].get("id")
84
+ if not task_run_id:
85
+ raise TaskRunsError("Task run ID not found in response")
86
+
87
+ logger.debug("Got latest task run ID", task_run_id=task_run_id)
88
+ return task_run_id
89
+
90
+ except httpx.TimeoutException as e:
91
+ logger.error("Latest task run request timed out", error=str(e))
92
+ raise TaskRunsError("Request timed out") from e
93
+ except httpx.RequestError as e:
94
+ logger.error("Latest task run request failed", error=str(e))
95
+ raise TaskRunsError(f"Request failed: {str(e)}") from e
96
+ except TaskRunsError:
97
+ raise
98
+ except Exception as e:
99
+ logger.error("Unexpected error getting latest task run", error=str(e))
100
+ raise TaskRunsError(f"Unexpected error: {str(e)}") from e
@@ -1,9 +1,12 @@
1
1
  """Tool for running manual tasks on a connected mobile device."""
2
2
 
3
3
  from collections.abc import Mapping
4
+ from pathlib import Path
4
5
  from typing import Any
5
6
 
6
7
  from fastmcp.exceptions import ToolError
8
+ from fastmcp.tools.tool import ToolResult
9
+ from mcp.types import TextContent
7
10
  from minitap.mobile_use.sdk.types import ManualTaskConfig
8
11
  from minitap.mobile_use.sdk.types.task import PlatformTaskRequest
9
12
  from pydantic import Field
@@ -11,10 +14,15 @@ from pydantic import Field
11
14
  from minitap.mcp.core.cloud_apk import install_apk_on_cloud_mobile, upload_apk_to_cloud_mobile
12
15
  from minitap.mcp.core.config import settings
13
16
  from minitap.mcp.core.decorators import handle_tool_errors
17
+ from minitap.mcp.core.logging_config import get_logger
14
18
  from minitap.mcp.core.sdk_agent import get_mobile_use_agent
19
+ from minitap.mcp.core.storage import StorageDownloadError, download_trajectory_gif
20
+ from minitap.mcp.core.task_runs import TaskRunsError, get_latest_task_run_id
15
21
  from minitap.mcp.main import mcp
16
22
  from minitap.mcp.server.cloud_mobile import check_cloud_mobile_status
17
23
 
24
+ logger = get_logger(__name__)
25
+
18
26
 
19
27
  def _serialize_result(result: Any) -> Any:
20
28
  """Convert SDK responses to serializable data for MCP."""
@@ -77,7 +85,7 @@ async def execute_mobile_command(
77
85
  "The APK will be uploaded to cloud storage and installed before task execution. "
78
86
  "Requires MINITAP_API_KEY to be configured. ",
79
87
  ),
80
- ) -> str | dict[str, Any]:
88
+ ) -> str | dict[str, Any] | ToolResult:
81
89
  """Run a manual task on a mobile device via the Minitap platform."""
82
90
  try:
83
91
  if settings.CLOUD_MOBILE_NAME:
@@ -110,6 +118,65 @@ async def execute_mobile_command(
110
118
  request=request,
111
119
  locked_app_package=locked_app_package,
112
120
  )
113
- return _serialize_result(result)
121
+
122
+ trajectory_gif_path: Path | None = None
123
+ if settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER:
124
+ trajectory_gif_path = await _download_trajectory_gif_if_available()
125
+
126
+ serialized_result = _serialize_result(result)
127
+
128
+ # If trajectory was saved, return a ToolResult with multiple content items
129
+ if trajectory_gif_path:
130
+ import json
131
+
132
+ result_text = (
133
+ json.dumps(serialized_result, indent=2)
134
+ if isinstance(serialized_result, dict)
135
+ else str(serialized_result)
136
+ )
137
+ return ToolResult(
138
+ content=[
139
+ TextContent(type="text", text=result_text),
140
+ TextContent(type="text", text=f"Trajectory saved to {trajectory_gif_path}"),
141
+ ],
142
+ )
143
+
144
+ return serialized_result
114
145
  except Exception as e:
115
146
  raise ToolError(str(e))
147
+
148
+
149
+ async def _download_trajectory_gif_if_available() -> Path | None:
150
+ """Download the trajectory GIF if available and folder is configured.
151
+
152
+ Fetches the latest task run ID from the API and downloads the GIF.
153
+
154
+ Returns:
155
+ The path to the downloaded GIF file, or None if download failed or not configured.
156
+ """
157
+ download_folder = settings.TRAJECTORY_GIF_DOWNLOAD_FOLDER
158
+ if not download_folder:
159
+ logger.warning("TRAJECTORY_GIF_DOWNLOAD_FOLDER not configured, skipping GIF download")
160
+ return None
161
+
162
+ task_run_id = None
163
+ try:
164
+ task_run_id = await get_latest_task_run_id()
165
+
166
+ gif_path = await download_trajectory_gif(
167
+ task_run_id=task_run_id,
168
+ download_path=download_folder,
169
+ )
170
+ logger.info(
171
+ "Trajectory GIF downloaded",
172
+ task_run_id=task_run_id,
173
+ path=str(gif_path),
174
+ )
175
+ return gif_path
176
+ except (StorageDownloadError, TaskRunsError) as e:
177
+ logger.warning(
178
+ "Failed to download trajectory GIF",
179
+ task_run_id=task_run_id,
180
+ error=str(e),
181
+ )
182
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: minitap-mcp
3
- Version: 0.8.1
3
+ Version: 0.9.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,15 +7,16 @@ minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_
7
7
  minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/model_params.json,sha256=p93kbUGxLYwc4NAEzPTBDA2XZ1ucsa7yevXZRe6V4Mc,37
8
8
  minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/output.md,sha256=Q0_5w9x5WAMqx6-I7KgjlS-tt7HnWKIunP43J7F0W_o,3703
9
9
  minitap/mcp/core/agents/compare_screenshots/prompt.md,sha256=qAyqOroSJROgrvlbsLCtiwFyBKuIMCQ-720A5cwgwPY,3563
10
- minitap/mcp/core/cloud_apk.py,sha256=Xeg1jrycm50UC72J4qVZ2NS9qs20oXKhvBcmBRs4rEA,3896
11
- minitap/mcp/core/config.py,sha256=aX4dGz-tZ88AVX6AeoBcEMUT-iVlabgtcwsCf47dSgY,3113
10
+ minitap/mcp/core/cloud_apk.py,sha256=K0PmmkvkCzYo-eRfwGmfPcBbMy8KD32pHvrehm_L_YM,4308
11
+ minitap/mcp/core/config.py,sha256=dCQMuT-jvGHBxIsvTQoUDOvZ98MTO5iwgCc3_bngX5M,4407
12
12
  minitap/mcp/core/decorators.py,sha256=ipzR7kbMXacG91f6CliN-nl9unRTtjmANrfueaOXJ2s,3591
13
13
  minitap/mcp/core/device.py,sha256=0AU8qGi26axC6toqHrPIzNeDbNDtll0YRwkspHouPmM,8198
14
14
  minitap/mcp/core/llm.py,sha256=tI5m5rFDLeMkXE5WExnzYSzHU3nTIEiSC9nAsPzVMaU,1144
15
- minitap/mcp/core/logging_config.py,sha256=OJlArPJxflbhckerFsRHVTzy3jwsLsNSPN0LVpkmpNM,1861
15
+ minitap/mcp/core/logging_config.py,sha256=GElnKsOFtk58EMJWPKRGtKW101tnCubNKRFTe0gEh4I,2020
16
16
  minitap/mcp/core/models.py,sha256=egLScxPAMo4u5cqY33UKba7z7DsdgqfPW409UAqW1Jg,1942
17
17
  minitap/mcp/core/sdk_agent.py,sha256=5YEr7aDjoiwbRQkZBK3jDa08c5QhPwLxahzlYrEB_KE,1132
18
- minitap/mcp/core/storage.py,sha256=y92pnE0pfLtH4nDKHZeUAZm_udR5sbRQGWbw13mjd_U,8189
18
+ minitap/mcp/core/storage.py,sha256=t9BAEqXH7Nu8p8hgGIk3mO6rxLwviBP0FqvQD4Lz8CQ,13184
19
+ minitap/mcp/core/task_runs.py,sha256=vGv8G-oZcfe_lpMOGiM649u10WNzoU3uWDgYWm1owdQ,3115
19
20
  minitap/mcp/core/utils/figma.py,sha256=L5aAHm59mrRYaqrwMJSM24SSdZPu2yVg-wsHTF3L8vk,2310
20
21
  minitap/mcp/core/utils/images.py,sha256=3uExpRoh7affIieZx3TLlZTmZCcoxWfx1YpPbwhjiJY,1791
21
22
  minitap/mcp/main.py,sha256=p5E_dBIBiS_sp_v9u8gNxjRGwl25T4De-ivZQbSqJa8,11269
@@ -23,12 +24,12 @@ minitap/mcp/server/cloud_mobile.py,sha256=4cKnUzOJE5tBzgXgJXmyH0ESf6OyCTlNllputE
23
24
  minitap/mcp/server/middleware.py,sha256=SjPc4pcfPuG0TnaDH7a19DS_HRFPl3bkbovdOLzy_IU,768
24
25
  minitap/mcp/server/poller.py,sha256=JsdW6nvj4r3tsn8AaTwXD4H9dVAAau4BhJXHXHit9nA,2528
25
26
  minitap/mcp/server/remote_proxy.py,sha256=IM7UfjbJlQRpFD_tdpdck1mFT1QOnlxj5OA1nS4tRhQ,3073
26
- minitap/mcp/tools/execute_mobile_command.py,sha256=YXHLKvNFmuvwh7d_kPYw0R64bHzKCjyqp38adx8jWWI,4453
27
+ minitap/mcp/tools/execute_mobile_command.py,sha256=K5KhSAqLdcmc6WH_u2TKAybECPw53TuSLEoSVrGCPOE,6776
27
28
  minitap/mcp/tools/read_swift_logs.py,sha256=Wc1XqQWWuNuPEIBioYD2geVd1p9Yq2USik6SX47Fq9A,9285
28
29
  minitap/mcp/tools/screen_analyzer.md,sha256=TTO80JQWusbA9cKAZn-9cqhgVHm6F_qJh5w152hG3YM,734
29
30
  minitap/mcp/tools/take_screenshot.py,sha256=gGySPSeVnx8lHiseGF_Wat82JLF-D8GuQIJ_hCaLZlQ,1730
30
31
  minitap/mcp/tools/upload_screenshot.py,sha256=kwh8Q46LWF3nyKbKlvnlf-CtGrPkctXSnLyeebQGNFI,2959
31
- minitap_mcp-0.8.1.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
32
- minitap_mcp-0.8.1.dist-info/entry_points.txt,sha256=rYVoXm7tSQCqQTtHx4Lovgn1YsjwtEEHfddKrfEVHuY,55
33
- minitap_mcp-0.8.1.dist-info/METADATA,sha256=KTCif4x5DUhUq3yhquy4XzZ2N03qkiVUp-aOxC12GSs,10619
34
- minitap_mcp-0.8.1.dist-info/RECORD,,
32
+ minitap_mcp-0.9.1.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
33
+ minitap_mcp-0.9.1.dist-info/entry_points.txt,sha256=rYVoXm7tSQCqQTtHx4Lovgn1YsjwtEEHfddKrfEVHuY,55
34
+ minitap_mcp-0.9.1.dist-info/METADATA,sha256=3zTwoaqHLDMUhftH78b7rO284NkyoBgBoOBPvuttzRM,10619
35
+ minitap_mcp-0.9.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.26
2
+ Generator: uv 0.9.28
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any