minitap-mcp 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- minitap/mcp/__init__.py +0 -0
- minitap/mcp/core/agents/compare_screenshots/agent.py +75 -0
- minitap/mcp/core/agents/compare_screenshots/eval/prompts/prompt_1.md +62 -0
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/actual.png +0 -0
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/figma.png +0 -0
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/human_feedback.txt +18 -0
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/model_params.json +3 -0
- minitap/mcp/core/agents/compare_screenshots/eval/scenario_1_add_cartoon_img_and_move_button/prompt_1/output.md +46 -0
- minitap/mcp/core/agents/compare_screenshots/prompt.md +62 -0
- minitap/mcp/core/cloud_apk.py +117 -0
- minitap/mcp/core/config.py +111 -0
- minitap/mcp/core/decorators.py +107 -0
- minitap/mcp/core/device.py +249 -0
- minitap/mcp/core/llm.py +39 -0
- minitap/mcp/core/logging_config.py +59 -0
- minitap/mcp/core/models.py +59 -0
- minitap/mcp/core/sdk_agent.py +35 -0
- minitap/mcp/core/storage.py +407 -0
- minitap/mcp/core/task_runs.py +100 -0
- minitap/mcp/core/utils/figma.py +69 -0
- minitap/mcp/core/utils/images.py +55 -0
- minitap/mcp/main.py +328 -0
- minitap/mcp/server/cloud_mobile.py +492 -0
- minitap/mcp/server/middleware.py +21 -0
- minitap/mcp/server/poller.py +78 -0
- minitap/mcp/server/remote_proxy.py +96 -0
- minitap/mcp/tools/execute_mobile_command.py +182 -0
- minitap/mcp/tools/read_swift_logs.py +297 -0
- minitap/mcp/tools/screen_analyzer.md +17 -0
- minitap/mcp/tools/take_screenshot.py +53 -0
- minitap/mcp/tools/upload_screenshot.py +80 -0
- minitap_mcp-0.9.0.dist-info/METADATA +352 -0
- minitap_mcp-0.9.0.dist-info/RECORD +35 -0
- minitap_mcp-0.9.0.dist-info/WHEEL +4 -0
- minitap_mcp-0.9.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Device detection and screenshot utilities for Android and iOS devices."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from adbutils import AdbClient, AdbDevice
|
|
12
|
+
from pydantic import BaseModel, ConfigDict
|
|
13
|
+
|
|
14
|
+
DevicePlatform = Literal["android", "ios"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MobileDevice(BaseModel):
|
|
18
|
+
"""Represents a mobile device with its platform and connection details."""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
21
|
+
|
|
22
|
+
device_id: str
|
|
23
|
+
platform: DevicePlatform
|
|
24
|
+
adb_device: AdbDevice | None = None # Only for Android
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DeviceInfo(BaseModel):
|
|
28
|
+
"""Serializable device information."""
|
|
29
|
+
|
|
30
|
+
device_id: str
|
|
31
|
+
platform: DevicePlatform
|
|
32
|
+
name: str | None = None
|
|
33
|
+
state: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DeviceNotFoundError(Exception):
|
|
37
|
+
"""Raised when no device can be found."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DeviceNotReadyError(Exception):
|
|
43
|
+
"""Raised when a device exists but is not ready (e.g., still starting)."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, state: str | None = None):
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.state = state
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_adb_client() -> AdbClient:
|
|
51
|
+
"""Get an ADB client instance."""
|
|
52
|
+
custom_adb_socket = os.getenv("ADB_SERVER_SOCKET")
|
|
53
|
+
if not custom_adb_socket:
|
|
54
|
+
return AdbClient()
|
|
55
|
+
parts = custom_adb_socket.split(":")
|
|
56
|
+
if len(parts) != 3:
|
|
57
|
+
raise ValueError(f"Invalid ADB server socket: {custom_adb_socket}")
|
|
58
|
+
_, host, port = parts
|
|
59
|
+
return AdbClient(host=host, port=int(port))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def list_available_devices() -> list[DeviceInfo]:
|
|
63
|
+
"""
|
|
64
|
+
List all available mobile devices (Android and iOS).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
list[DeviceInfo]: A list of device information objects.
|
|
68
|
+
"""
|
|
69
|
+
devices: list[DeviceInfo] = []
|
|
70
|
+
|
|
71
|
+
# List Android devices
|
|
72
|
+
try:
|
|
73
|
+
adb_client = get_adb_client()
|
|
74
|
+
android_devices = adb_client.device_list()
|
|
75
|
+
|
|
76
|
+
for device in android_devices:
|
|
77
|
+
if device.serial:
|
|
78
|
+
devices.append(
|
|
79
|
+
DeviceInfo(
|
|
80
|
+
device_id=device.serial,
|
|
81
|
+
platform="android",
|
|
82
|
+
name=device.serial,
|
|
83
|
+
state="connected",
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
# ADB not available or error listing devices
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
# List iOS devices (only booted simulators to match SDK behavior)
|
|
91
|
+
try:
|
|
92
|
+
cmd = ["xcrun", "simctl", "list", "devices", "booted", "-j"]
|
|
93
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
94
|
+
data = json.loads(result.stdout)
|
|
95
|
+
|
|
96
|
+
for runtime, ios_devices in data.get("devices", {}).items():
|
|
97
|
+
if "iOS" not in runtime:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
for device in ios_devices:
|
|
101
|
+
udid = device.get("udid")
|
|
102
|
+
name = device.get("name")
|
|
103
|
+
state = device.get("state")
|
|
104
|
+
|
|
105
|
+
if udid and state == "Booted":
|
|
106
|
+
devices.append(
|
|
107
|
+
DeviceInfo(
|
|
108
|
+
device_id=udid,
|
|
109
|
+
platform="ios",
|
|
110
|
+
name=name,
|
|
111
|
+
state=state,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
except (subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError):
|
|
115
|
+
# xcrun not available or error listing devices
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
return devices
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def find_mobile_device(device_id: str | None = None) -> MobileDevice:
|
|
122
|
+
"""
|
|
123
|
+
Find a mobile device (Android via ADB or iOS via xcrun).
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
device_id: Optional device ID to target a specific device.
|
|
127
|
+
If None, returns the first available device.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
MobileDevice: A reference to the device with its platform information.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
DeviceNotFoundError: If no device is found or the specified device_id is not found.
|
|
134
|
+
"""
|
|
135
|
+
# Get all available devices
|
|
136
|
+
available_devices = list_available_devices()
|
|
137
|
+
|
|
138
|
+
if not available_devices:
|
|
139
|
+
raise DeviceNotFoundError(
|
|
140
|
+
"No mobile device found. "
|
|
141
|
+
"Make sure you have an Android device connected via ADB "
|
|
142
|
+
"or an iOS simulator running."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Find the target device
|
|
146
|
+
target_device = None
|
|
147
|
+
if device_id:
|
|
148
|
+
# Look for specific device
|
|
149
|
+
for dev in available_devices:
|
|
150
|
+
if dev.device_id == device_id:
|
|
151
|
+
target_device = dev
|
|
152
|
+
break
|
|
153
|
+
if not target_device:
|
|
154
|
+
raise DeviceNotFoundError(
|
|
155
|
+
f"Device with ID '{device_id}' not found. "
|
|
156
|
+
"Make sure the device is connected and accessible via adb or xcrun."
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
# Prefer connected/booted devices first
|
|
160
|
+
for dev in available_devices:
|
|
161
|
+
if dev.state in ("connected", "Booted"):
|
|
162
|
+
target_device = dev
|
|
163
|
+
break
|
|
164
|
+
# Fall back to any device if no connected/booted device found
|
|
165
|
+
if not target_device:
|
|
166
|
+
target_device = available_devices[0]
|
|
167
|
+
|
|
168
|
+
# Create MobileDevice instance with platform-specific details
|
|
169
|
+
if target_device.platform == "android":
|
|
170
|
+
# For Android, get the AdbDevice reference
|
|
171
|
+
try:
|
|
172
|
+
adb_client = get_adb_client()
|
|
173
|
+
adb_device = adb_client.device(serial=target_device.device_id)
|
|
174
|
+
return MobileDevice(
|
|
175
|
+
device_id=target_device.device_id,
|
|
176
|
+
platform="android",
|
|
177
|
+
adb_device=adb_device,
|
|
178
|
+
)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
raise DeviceNotFoundError(f"Failed to connect to Android device: {e}")
|
|
181
|
+
else:
|
|
182
|
+
# For iOS, just return the device info
|
|
183
|
+
return MobileDevice(device_id=target_device.device_id, platform="ios")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def capture_screenshot(device: MobileDevice) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Capture a screenshot from the given mobile device.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
device: MobileDevice instance returned by find_mobile_device()
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
str: Base64-encoded screenshot image (PNG format)
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
RuntimeError: If screenshot capture fails
|
|
198
|
+
"""
|
|
199
|
+
if device.platform == "android":
|
|
200
|
+
return _capture_android_screenshot(device)
|
|
201
|
+
else:
|
|
202
|
+
return _capture_ios_screenshot(device)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _capture_android_screenshot(device: MobileDevice) -> str:
|
|
206
|
+
"""Capture screenshot from Android device using ADB."""
|
|
207
|
+
if not device.adb_device:
|
|
208
|
+
# Reconnect to device if not available
|
|
209
|
+
adb_client = get_adb_client()
|
|
210
|
+
adb_device = adb_client.device(serial=device.device_id)
|
|
211
|
+
if not adb_device:
|
|
212
|
+
raise RuntimeError(f"Android device {device.device_id} not found")
|
|
213
|
+
device.adb_device = adb_device
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Use ADB screencap to get PNG screenshot
|
|
217
|
+
screenshot_bytes = device.adb_device.shell("screencap -p", encoding=None)
|
|
218
|
+
if isinstance(screenshot_bytes, bytes):
|
|
219
|
+
return base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
220
|
+
else:
|
|
221
|
+
raise RuntimeError("Unexpected screenshot data type from ADB")
|
|
222
|
+
except Exception as e:
|
|
223
|
+
raise RuntimeError(f"Failed to capture Android screenshot: {e}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _capture_ios_screenshot(device: MobileDevice) -> str:
|
|
227
|
+
"""Capture screenshot from iOS simulator using xcrun."""
|
|
228
|
+
try:
|
|
229
|
+
# Create temporary file for screenshot
|
|
230
|
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_file:
|
|
231
|
+
tmp_path = Path(tmp_file.name)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
# Capture screenshot using xcrun simctl
|
|
235
|
+
cmd = ["xcrun", "simctl", "io", device.device_id, "screenshot", str(tmp_path)]
|
|
236
|
+
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
237
|
+
|
|
238
|
+
# Read and encode the screenshot
|
|
239
|
+
screenshot_bytes = tmp_path.read_bytes()
|
|
240
|
+
return base64.b64encode(screenshot_bytes).decode("utf-8")
|
|
241
|
+
finally:
|
|
242
|
+
# Clean up temporary file
|
|
243
|
+
if tmp_path.exists():
|
|
244
|
+
tmp_path.unlink()
|
|
245
|
+
|
|
246
|
+
except subprocess.CalledProcessError as e:
|
|
247
|
+
raise RuntimeError(f"Failed to capture iOS screenshot: {e.stderr}")
|
|
248
|
+
except Exception as e:
|
|
249
|
+
raise RuntimeError(f"Failed to capture iOS screenshot: {e}")
|
minitap/mcp/core/llm.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from langchain_openai import ChatOpenAI
|
|
2
|
+
|
|
3
|
+
from minitap.mcp.core.config import settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_minitap_llm(
|
|
7
|
+
trace_id: str,
|
|
8
|
+
remote_tracing: bool = False,
|
|
9
|
+
model: str = "google/gemini-2.5-pro",
|
|
10
|
+
temperature: float | None = None,
|
|
11
|
+
max_retries: int | None = None,
|
|
12
|
+
) -> ChatOpenAI:
|
|
13
|
+
assert settings.MINITAP_API_KEY is not None
|
|
14
|
+
assert settings.MINITAP_API_BASE_URL is not None
|
|
15
|
+
if max_retries is None and model.startswith("google/"):
|
|
16
|
+
max_retries = 2
|
|
17
|
+
client = ChatOpenAI(
|
|
18
|
+
model=model,
|
|
19
|
+
temperature=temperature,
|
|
20
|
+
max_retries=max_retries,
|
|
21
|
+
api_key=settings.MINITAP_API_KEY,
|
|
22
|
+
base_url=settings.MINITAP_API_BASE_URL,
|
|
23
|
+
default_query={
|
|
24
|
+
"sessionId": trace_id,
|
|
25
|
+
"traceOnlyUsage": remote_tracing,
|
|
26
|
+
},
|
|
27
|
+
)
|
|
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)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from minitap.mobile_use.sdk import Agent
|
|
4
|
+
from minitap.mobile_use.sdk.builders import Builders
|
|
5
|
+
|
|
6
|
+
from minitap.mcp.core.config import settings
|
|
7
|
+
|
|
8
|
+
# Lazy-initialized singleton agent
|
|
9
|
+
_agent: Agent | None = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_mobile_use_agent() -> Agent:
|
|
13
|
+
"""Get or create the mobile-use agent singleton.
|
|
14
|
+
|
|
15
|
+
This function lazily initializes the agent on first call, ensuring
|
|
16
|
+
that CLI arguments are parsed before agent creation.
|
|
17
|
+
"""
|
|
18
|
+
global _agent
|
|
19
|
+
if _agent is None:
|
|
20
|
+
config = Builders.AgentConfig
|
|
21
|
+
custom_adb_socket = os.getenv("ADB_SERVER_SOCKET")
|
|
22
|
+
if custom_adb_socket:
|
|
23
|
+
parts = custom_adb_socket.split(":")
|
|
24
|
+
if len(parts) != 3:
|
|
25
|
+
raise ValueError(f"Invalid ADB server socket: {custom_adb_socket}")
|
|
26
|
+
_, host, port = parts
|
|
27
|
+
config = config.with_adb_server(host=host, port=int(port))
|
|
28
|
+
|
|
29
|
+
# Add cloud mobile configuration if set
|
|
30
|
+
if settings.CLOUD_MOBILE_NAME:
|
|
31
|
+
config = config.for_cloud_mobile(cloud_mobile_id_or_ref=settings.CLOUD_MOBILE_NAME)
|
|
32
|
+
|
|
33
|
+
_agent = Agent(config=config.build())
|
|
34
|
+
|
|
35
|
+
return _agent
|