uiautomator2-mcp-server 0.1.3__py3-none-any.whl → 0.2.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.
- u2mcp/.gitignore +1 -1
- u2mcp/__init__.py +1 -2
- u2mcp/__main__.py +168 -57
- u2mcp/background.py +19 -0
- u2mcp/health.py +67 -0
- u2mcp/helpers.py +222 -0
- u2mcp/mcp.py +111 -18
- u2mcp/middlewares.py +40 -0
- u2mcp/tools/__init__.py +4 -0
- u2mcp/tools/action.py +9 -35
- u2mcp/tools/app.py +13 -13
- u2mcp/tools/clipboard.py +35 -0
- u2mcp/tools/device.py +119 -71
- u2mcp/tools/element.py +267 -0
- u2mcp/tools/input.py +47 -0
- u2mcp/tools/misc.py +1 -1
- u2mcp/tools/scrcpy.py +142 -0
- u2mcp/{_version.py → version.py} +2 -2
- uiautomator2_mcp_server-0.2.0.dist-info/METADATA +738 -0
- uiautomator2_mcp_server-0.2.0.dist-info/RECORD +25 -0
- {uiautomator2_mcp_server-0.1.3.dist-info → uiautomator2_mcp_server-0.2.0.dist-info}/WHEEL +1 -1
- {uiautomator2_mcp_server-0.1.3.dist-info → uiautomator2_mcp_server-0.2.0.dist-info}/entry_points.txt +1 -0
- uiautomator2_mcp_server-0.2.0.dist-info/licenses/LICENSE +190 -0
- uiautomator2_mcp_server-0.1.3.dist-info/METADATA +0 -115
- uiautomator2_mcp_server-0.1.3.dist-info/RECORD +0 -17
- uiautomator2_mcp_server-0.1.3.dist-info/licenses/LICENSE +0 -620
- {uiautomator2_mcp_server-0.1.3.dist-info → uiautomator2_mcp_server-0.2.0.dist-info}/top_level.txt +0 -0
u2mcp/tools/scrcpy.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from anyio import create_task_group, move_on_after, open_process
|
|
6
|
+
from anyio.abc import AnyByteReceiveStream, Process
|
|
7
|
+
from anyio.streams.text import TextReceiveStream
|
|
8
|
+
from fastmcp.server.dependencies import get_context
|
|
9
|
+
from fastmcp.utilities.logging import get_logger
|
|
10
|
+
|
|
11
|
+
from ..background import get_background_task_group
|
|
12
|
+
from ..mcp import mcp
|
|
13
|
+
|
|
14
|
+
__all__ = ("start_scrcpy", "stop_scrcpy")
|
|
15
|
+
|
|
16
|
+
# Keep track of background scrcpy processes
|
|
17
|
+
_background_processes: dict[int, Process] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp.tool("start_scrcpy", tags={"screen:mirror"})
|
|
21
|
+
async def start_scrcpy(serial: str = "", timeout: float = 5.0) -> int:
|
|
22
|
+
"""Startup scrcpy in background and returns process id.
|
|
23
|
+
|
|
24
|
+
scrcpy is an application mirrors Android devices (video and audio) connected via USB or TCP/IP and allows control using the computer's keyboard and mouse.
|
|
25
|
+
|
|
26
|
+
The scrcpy process will run in the background after successful startup.
|
|
27
|
+
Use stop_scrcpy to terminate the process.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
serial (str): Android device serialno. If empty string, connects to the unique device if only one device is connected.
|
|
31
|
+
timeout (float): Seconds to wait for process to confirm startup.
|
|
32
|
+
If process is still running after this time, startup is considered successful.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
int: process id (pid)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
logger = get_logger(f"{__name__}.start_scrcpy")
|
|
39
|
+
ctx = get_context()
|
|
40
|
+
|
|
41
|
+
scrcpy_path = os.environ.get("SCRCPY", "scrcpy.exe" if os.name == "nt" else "scrcpy")
|
|
42
|
+
command = [scrcpy_path]
|
|
43
|
+
if serial := serial.strip():
|
|
44
|
+
command.extend(["--serial", serial])
|
|
45
|
+
|
|
46
|
+
logger.info("start scrcpy: %s", command)
|
|
47
|
+
|
|
48
|
+
process = await open_process(command)
|
|
49
|
+
pid = process.pid
|
|
50
|
+
|
|
51
|
+
# Stream monitoring function
|
|
52
|
+
async def receive(name: str, stream: AnyByteReceiveStream):
|
|
53
|
+
logger.info("Starting to monitor %s for pid=%s", name, pid)
|
|
54
|
+
async for line in TextReceiveStream(stream):
|
|
55
|
+
logger.debug("Received %s line: %s", name, line.strip())
|
|
56
|
+
match name:
|
|
57
|
+
case "stdout":
|
|
58
|
+
await ctx.info(line)
|
|
59
|
+
case "stderr":
|
|
60
|
+
await ctx.error(line)
|
|
61
|
+
case _:
|
|
62
|
+
raise ValueError(f"Unknown stream name: {name}")
|
|
63
|
+
logger.info("Finished monitoring %s for pid=%s", name, pid)
|
|
64
|
+
|
|
65
|
+
# Monitor streams and auto-cleanup on process exit
|
|
66
|
+
async def monitor_streams():
|
|
67
|
+
if process.stdout is None:
|
|
68
|
+
raise RuntimeError("stdout is None")
|
|
69
|
+
if process.stderr is None:
|
|
70
|
+
raise RuntimeError("stderr is None")
|
|
71
|
+
try:
|
|
72
|
+
async with create_task_group() as inner_tg:
|
|
73
|
+
for name, handle in zip(("stdout", "stderr"), (process.stdout, process.stderr)):
|
|
74
|
+
inner_tg.start_soon(receive, name, handle)
|
|
75
|
+
finally:
|
|
76
|
+
# Cleanup only if we still own the process (i.e., not manually stopped)
|
|
77
|
+
if _background_processes.pop(pid, None) is process:
|
|
78
|
+
logger.info("scrcpy process exited naturally, cleaning up (pid=%s)", pid)
|
|
79
|
+
await process.aclose()
|
|
80
|
+
else:
|
|
81
|
+
logger.info("scrcpy process was manually stopped, skipping cleanup (pid=%s)", pid)
|
|
82
|
+
|
|
83
|
+
# Start monitoring immediately (before startup wait) to capture startup logs
|
|
84
|
+
tg = get_background_task_group()
|
|
85
|
+
if tg is None:
|
|
86
|
+
raise RuntimeError("Monitor task group not initialized - server not started?")
|
|
87
|
+
tg.start_soon(monitor_streams)
|
|
88
|
+
|
|
89
|
+
# Store process
|
|
90
|
+
_background_processes[pid] = process
|
|
91
|
+
|
|
92
|
+
# Startup phase: wait for process exit with timeout
|
|
93
|
+
with move_on_after(timeout) as timeout_scope:
|
|
94
|
+
await process.wait()
|
|
95
|
+
|
|
96
|
+
if timeout_scope.cancel_called:
|
|
97
|
+
# Timeout reached, process is still running - success!
|
|
98
|
+
logger.info("scrcpy started successfully in background (pid=%s)", pid)
|
|
99
|
+
else:
|
|
100
|
+
# Process exited before timeout - failure
|
|
101
|
+
_background_processes.pop(pid, None)
|
|
102
|
+
if process.returncode == 0:
|
|
103
|
+
raise RuntimeError(f"scrcpy closed during startup (pid={pid}, return code 0)")
|
|
104
|
+
else:
|
|
105
|
+
raise RuntimeError(f"scrcpy exited during startup wait with code: {process.returncode} (pid={pid})")
|
|
106
|
+
|
|
107
|
+
return pid
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@mcp.tool("stop_scrcpy", tags={"screen:mirror"})
|
|
111
|
+
async def stop_scrcpy(pid: int, timeout: float = 5.0) -> None:
|
|
112
|
+
"""Stop a running scrcpy process by pid.
|
|
113
|
+
|
|
114
|
+
scrcpy is an application mirrors Android devices (video and audio) connected via USB or TCP/IP and allows control using the computer's keyboard and mouse.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
pid (int): Process id of the scrcpy process to stop
|
|
118
|
+
timeout (float): Seconds to wait for process to terminate.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
None
|
|
122
|
+
"""
|
|
123
|
+
logger = get_logger(f"{__name__}.stop_scrcpy")
|
|
124
|
+
|
|
125
|
+
if pid not in _background_processes:
|
|
126
|
+
raise ValueError(f"No scrcpy process found with pid: {pid}")
|
|
127
|
+
|
|
128
|
+
process = _background_processes.pop(pid)
|
|
129
|
+
try:
|
|
130
|
+
process.kill()
|
|
131
|
+
# Wait for process to exit with timeout
|
|
132
|
+
with move_on_after(timeout) as timeout_scope:
|
|
133
|
+
await process.wait()
|
|
134
|
+
finally:
|
|
135
|
+
# Always close the process when manually stopping
|
|
136
|
+
# The monitor task will detect we popped the process and skip cleanup
|
|
137
|
+
await process.aclose()
|
|
138
|
+
|
|
139
|
+
if timeout_scope.cancel_called:
|
|
140
|
+
logger.warning("scrcpy process did not exit within %ss (pid=%s)", timeout, pid)
|
|
141
|
+
else:
|
|
142
|
+
logger.info("scrcpy process stopped (pid=%s)", pid)
|
u2mcp/{_version.py → version.py}
RENAMED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|