uiautomator2-mcp-server 0.1.2__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 +193 -82
- u2mcp/background.py +19 -0
- u2mcp/health.py +67 -0
- u2mcp/helpers.py +222 -0
- u2mcp/mcp.py +172 -61
- u2mcp/middlewares.py +40 -0
- u2mcp/tools/__init__.py +8 -3
- u2mcp/tools/action.py +143 -169
- u2mcp/tools/app.py +232 -231
- u2mcp/tools/clipboard.py +35 -0
- u2mcp/tools/device.py +307 -293
- u2mcp/tools/element.py +267 -0
- u2mcp/tools/input.py +47 -0
- u2mcp/tools/misc.py +17 -0
- u2mcp/tools/scrcpy.py +142 -0
- u2mcp/{_version.py → version.py} +34 -34
- 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.2.dist-info → uiautomator2_mcp_server-0.2.0.dist-info}/WHEEL +1 -1
- {uiautomator2_mcp_server-0.1.2.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.2.dist-info/METADATA +0 -113
- uiautomator2_mcp_server-0.1.2.dist-info/RECORD +0 -16
- uiautomator2_mcp_server-0.1.2.dist-info/licenses/LICENSE +0 -620
- {uiautomator2_mcp_server-0.1.2.dist-info → uiautomator2_mcp_server-0.2.0.dist-info}/top_level.txt +0 -0
u2mcp/mcp.py
CHANGED
|
@@ -1,61 +1,172 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This MCP server provides tools for controlling and interacting with Android devices using uiautomator2.
|
|
3
|
-
|
|
4
|
-
It allows you to perform various operations on Android devices such as connecting to devices, taking screenshots,
|
|
5
|
-
getting device information, accessing UI hierarchy, tap on screens, and more...
|
|
6
|
-
|
|
7
|
-
It also provides tools for managing Android applications, such as installing, uninstalling, starting, stopping, and clearing applications.
|
|
8
|
-
|
|
9
|
-
Before performing operations on a device, you need to initialize it using the init tool.
|
|
10
|
-
|
|
11
|
-
All operations require a device serial number to identify the target device.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
1
|
+
"""
|
|
2
|
+
This MCP server provides tools for controlling and interacting with Android devices using uiautomator2.
|
|
3
|
+
|
|
4
|
+
It allows you to perform various operations on Android devices such as connecting to devices, taking screenshots,
|
|
5
|
+
getting device information, accessing UI hierarchy, tap on screens, and more...
|
|
6
|
+
|
|
7
|
+
It also provides tools for managing Android applications, such as installing, uninstalling, starting, stopping, and clearing applications.
|
|
8
|
+
|
|
9
|
+
Before performing operations on a device, you need to initialize it using the init tool.
|
|
10
|
+
|
|
11
|
+
All operations require a device serial number to identify the target device.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import fnmatch
|
|
17
|
+
import sys
|
|
18
|
+
from contextlib import asynccontextmanager
|
|
19
|
+
from functools import partial
|
|
20
|
+
from textwrap import dedent
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from anyio import create_task_group
|
|
24
|
+
from fastmcp import FastMCP
|
|
25
|
+
from fastmcp.server.auth import AccessToken, AuthProvider
|
|
26
|
+
from pydantic import AnyHttpUrl
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
from rich.markdown import Markdown
|
|
29
|
+
|
|
30
|
+
from .background import set_background_task_group
|
|
31
|
+
from .helpers import print_tags
|
|
32
|
+
from .middlewares import EmptyResponseMiddleware
|
|
33
|
+
|
|
34
|
+
if sys.version_info >= (3, 12): # qa: noqa
|
|
35
|
+
from typing import override
|
|
36
|
+
else: # qa: noqa
|
|
37
|
+
from typing_extensions import override
|
|
38
|
+
|
|
39
|
+
__all__ = ["mcp", "make_mcp"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Warning: You can NOT import it unless call `make_mcp()`
|
|
43
|
+
mcp: FastMCP
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_tags(tags: str | None) -> set[str] | None:
|
|
47
|
+
"""Parse comma-separated tags string into a set."""
|
|
48
|
+
if not tags:
|
|
49
|
+
return None
|
|
50
|
+
return {tag.strip() for tag in tags.split(",") if tag.strip()}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _expand_wildcards(tags: set[str] | None, all_available_tags: set[str] | None) -> set[str] | None:
|
|
54
|
+
"""Expand wildcard patterns in tags.
|
|
55
|
+
|
|
56
|
+
Supports:
|
|
57
|
+
- * matches any characters
|
|
58
|
+
- ? matches exactly one character
|
|
59
|
+
- device:* matches all device:* tags
|
|
60
|
+
- *:shell matches all shell tags (device:shell, etc.)
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
device:* -> device:manage, device:info, device:capture, device:shell
|
|
64
|
+
*:shell -> device:shell
|
|
65
|
+
action:to* -> action:touch, action:tool (if exists)
|
|
66
|
+
"""
|
|
67
|
+
if not tags or not all_available_tags:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
expanded = set()
|
|
71
|
+
|
|
72
|
+
for tag in tags:
|
|
73
|
+
if "*" in tag or "?" in tag:
|
|
74
|
+
# Use fnmatch for wildcard matching
|
|
75
|
+
for existing_tag in all_available_tags:
|
|
76
|
+
if fnmatch.fnmatch(existing_tag, tag):
|
|
77
|
+
expanded.add(existing_tag)
|
|
78
|
+
else:
|
|
79
|
+
# No wildcard, add as-is
|
|
80
|
+
expanded.add(tag)
|
|
81
|
+
|
|
82
|
+
return expanded if expanded else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@asynccontextmanager
|
|
86
|
+
async def _lifespan(instance: FastMCP, /, show_tags: bool = True, token: str | None = None):
|
|
87
|
+
console = Console(stderr=True)
|
|
88
|
+
|
|
89
|
+
# Show enabled tags and tools if requested
|
|
90
|
+
if show_tags:
|
|
91
|
+
console.print("\n[bold cyan]Enabled Tags and Tools:[/bold cyan]")
|
|
92
|
+
await print_tags(instance, console)
|
|
93
|
+
console.print("")
|
|
94
|
+
|
|
95
|
+
if token:
|
|
96
|
+
content = Markdown(
|
|
97
|
+
dedent(f"""
|
|
98
|
+
------
|
|
99
|
+
|
|
100
|
+
Server configured with **authentication token**. Connect using this token in the Authorization header:
|
|
101
|
+
|
|
102
|
+
`Authorization: Bearer {token}`
|
|
103
|
+
|
|
104
|
+
------
|
|
105
|
+
""")
|
|
106
|
+
)
|
|
107
|
+
console.print(content)
|
|
108
|
+
|
|
109
|
+
# Global task group for background tasks - keeps running until server shuts down
|
|
110
|
+
async with create_task_group() as tg:
|
|
111
|
+
set_background_task_group(tg)
|
|
112
|
+
yield
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class _SimpleTokenAuthProvider(AuthProvider):
|
|
116
|
+
@override
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
base_url: AnyHttpUrl | str | None = None,
|
|
120
|
+
required_scopes: list[str] | None = ["mcp:tools"],
|
|
121
|
+
token: str | None = None,
|
|
122
|
+
):
|
|
123
|
+
super().__init__(base_url, required_scopes)
|
|
124
|
+
self.token = token
|
|
125
|
+
|
|
126
|
+
@override
|
|
127
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
128
|
+
if self.token == token:
|
|
129
|
+
return AccessToken(token=token, client_id="user", scopes=self.required_scopes)
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def make_mcp(
|
|
134
|
+
token: str | None = None,
|
|
135
|
+
include_tags: str | None = None,
|
|
136
|
+
exclude_tags: str | None = None,
|
|
137
|
+
show_tags: bool = False,
|
|
138
|
+
fix_empty_responses: bool = False,
|
|
139
|
+
) -> FastMCP:
|
|
140
|
+
global mcp
|
|
141
|
+
params: dict[str, Any] = dict(name="uiautomator2", instructions=__doc__)
|
|
142
|
+
lifespan_kwargs: dict[str, Any] = {"show_tags": show_tags}
|
|
143
|
+
if token:
|
|
144
|
+
lifespan_kwargs["token"] = token
|
|
145
|
+
params.update(lifespan=partial(_lifespan, **lifespan_kwargs), auth=_SimpleTokenAuthProvider(token=token))
|
|
146
|
+
else:
|
|
147
|
+
params.update(lifespan=partial(_lifespan, **lifespan_kwargs))
|
|
148
|
+
mcp = FastMCP(**params)
|
|
149
|
+
|
|
150
|
+
# Add middleware to fix empty responses if enabled
|
|
151
|
+
if fix_empty_responses:
|
|
152
|
+
mcp.add_middleware(EmptyResponseMiddleware())
|
|
153
|
+
|
|
154
|
+
# Import tools to register them with the MCP (needed for wildcard expansion)
|
|
155
|
+
from . import tools as _ # noqa: F401
|
|
156
|
+
|
|
157
|
+
# Collect all available tags from registered tools for wildcard expansion
|
|
158
|
+
all_tag_set: set[str] = set()
|
|
159
|
+
for tool in mcp._tool_manager._tools.values():
|
|
160
|
+
all_tag_set.update(tool.tags or [])
|
|
161
|
+
|
|
162
|
+
# Parse and expand tag filters
|
|
163
|
+
parsed_include_tags = _expand_wildcards(_parse_tags(include_tags), all_tag_set)
|
|
164
|
+
parsed_exclude_tags = _expand_wildcards(_parse_tags(exclude_tags), all_tag_set)
|
|
165
|
+
|
|
166
|
+
# Set tag filters on the MCP instance
|
|
167
|
+
if parsed_include_tags is not None:
|
|
168
|
+
mcp.include_tags = parsed_include_tags
|
|
169
|
+
if parsed_exclude_tags is not None:
|
|
170
|
+
mcp.exclude_tags = parsed_exclude_tags
|
|
171
|
+
|
|
172
|
+
return mcp
|
u2mcp/middlewares.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Middleware to fix empty responses for Zhipu AI compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
|
|
6
|
+
from fastmcp.tools.tool import ToolResult
|
|
7
|
+
from mcp.types import CallToolRequestParams, TextContent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EmptyResponseMiddleware(Middleware):
|
|
11
|
+
"""Middleware that converts empty tool responses to non-empty values.
|
|
12
|
+
|
|
13
|
+
This is needed for compatibility with ZhiPu AI model service, which doesn't handle empty responses (content: []) correctly.
|
|
14
|
+
|
|
15
|
+
When enabled, this middleware will convert empty content to "" (empty string).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
async def on_call_tool(
|
|
19
|
+
self,
|
|
20
|
+
context: MiddlewareContext[CallToolRequestParams],
|
|
21
|
+
call_next: CallNext[CallToolRequestParams, ToolResult],
|
|
22
|
+
) -> ToolResult:
|
|
23
|
+
"""Intercept tool calls and fix empty responses."""
|
|
24
|
+
result = await call_next(context)
|
|
25
|
+
|
|
26
|
+
# Convert empty content to empty string for ZhiPu compatibility
|
|
27
|
+
if isinstance(result, ToolResult):
|
|
28
|
+
if not result.content:
|
|
29
|
+
# Empty content - return empty string
|
|
30
|
+
result.content = [TextContent(type="text", text="")]
|
|
31
|
+
elif (
|
|
32
|
+
isinstance(result.content, list)
|
|
33
|
+
and len(result.content) == 1
|
|
34
|
+
and isinstance(result.content[0], dict)
|
|
35
|
+
and not result.content[0].get("text")
|
|
36
|
+
):
|
|
37
|
+
# Missing/empty text - set to empty string
|
|
38
|
+
result.content[0]["text"] = ""
|
|
39
|
+
|
|
40
|
+
return result
|
u2mcp/tools/__init__.py
CHANGED
u2mcp/tools/action.py
CHANGED
|
@@ -1,169 +1,143 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
from ..mcp import mcp
|
|
6
|
-
from .device import get_device
|
|
7
|
-
|
|
8
|
-
__all__ = (
|
|
9
|
-
"click",
|
|
10
|
-
"long_click",
|
|
11
|
-
"double_click",
|
|
12
|
-
"swipe",
|
|
13
|
-
"swipe_points",
|
|
14
|
-
"drag",
|
|
15
|
-
"press_key",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
serial (str): Android device serialno
|
|
145
|
-
"""
|
|
146
|
-
async with get_device(serial) as device:
|
|
147
|
-
await asyncio.to_thread(device.clear_text)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
@mcp.tool("screen_on")
|
|
151
|
-
async def screen_on(serial: str):
|
|
152
|
-
"""Turn screen on
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
serial (str): Android device serialno
|
|
156
|
-
"""
|
|
157
|
-
async with get_device(serial) as device:
|
|
158
|
-
await asyncio.to_thread(device.screen_on)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
@mcp.tool("screen_off")
|
|
162
|
-
async def screen_off(serial: str):
|
|
163
|
-
"""Turn screen off
|
|
164
|
-
|
|
165
|
-
Args:
|
|
166
|
-
serial (str): Android device serialno
|
|
167
|
-
"""
|
|
168
|
-
async with get_device(serial) as device:
|
|
169
|
-
await asyncio.to_thread(device.screen_off)
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from anyio import to_thread
|
|
4
|
+
|
|
5
|
+
from ..mcp import mcp
|
|
6
|
+
from .device import get_device
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"click",
|
|
10
|
+
"long_click",
|
|
11
|
+
"double_click",
|
|
12
|
+
"swipe",
|
|
13
|
+
"swipe_points",
|
|
14
|
+
"drag",
|
|
15
|
+
"press_key",
|
|
16
|
+
"screen_on",
|
|
17
|
+
"screen_off",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@mcp.tool("click", tags={"action:touch"})
|
|
22
|
+
async def click(serial: str, x: int, y: int):
|
|
23
|
+
"""Click at specific coordinates
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
serial (str): Android device serialno
|
|
27
|
+
x (int): X coordinate
|
|
28
|
+
y (int): Y coordinate
|
|
29
|
+
"""
|
|
30
|
+
async with get_device(serial) as device:
|
|
31
|
+
await to_thread.run_sync(device.click, x, y)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mcp.tool("long_click", tags={"action:touch"})
|
|
35
|
+
async def long_click(serial: str, x: int, y: int, duration: float = 0.5):
|
|
36
|
+
"""Long click at specific coordinates
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
serial (str): Android device serialno
|
|
40
|
+
x (int): X coordinate
|
|
41
|
+
y (int): Y coordinate
|
|
42
|
+
duration (float): Duration of the long click in seconds, default is 0.5
|
|
43
|
+
"""
|
|
44
|
+
async with get_device(serial) as device:
|
|
45
|
+
await to_thread.run_sync(device.long_click, x, y, duration)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@mcp.tool("double_click", tags={"action:touch"})
|
|
49
|
+
async def double_click(serial: str, x: int, y: int, duration: float = 0.1):
|
|
50
|
+
"""Double click at specific coordinates
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
serial (str): Android device serialno
|
|
54
|
+
x (int): X coordinate
|
|
55
|
+
y (int): Y coordinate
|
|
56
|
+
duration (float): Duration between clicks in seconds, default is 0.1
|
|
57
|
+
"""
|
|
58
|
+
async with get_device(serial) as device:
|
|
59
|
+
await to_thread.run_sync(device.double_click, x, y, duration)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@mcp.tool("swipe", tags={"action:gesture"})
|
|
63
|
+
async def swipe(serial: str, fx: int, fy: int, tx: int, ty: int, duration: float = 0.0, step: int = 0):
|
|
64
|
+
"""Swipe from one point to another
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
serial (str): Android device serialno
|
|
68
|
+
fx (int): From position X coordinate
|
|
69
|
+
fy (int): From position Y coordinate
|
|
70
|
+
tx (int): To position X coordinate
|
|
71
|
+
ty (int): To position Y coordinate
|
|
72
|
+
duration (float): duration
|
|
73
|
+
steps: 1 steps is about 5ms, if set, duration will be ignore
|
|
74
|
+
"""
|
|
75
|
+
async with get_device(serial) as device:
|
|
76
|
+
await to_thread.run_sync(device.swipe, fx, fy, tx, ty, duration if duration > 0 else None, step if step > 0 else None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@mcp.tool("swipe_points", tags={"action:gesture"})
|
|
80
|
+
async def swipe_points(serial: str, points: list[tuple[int, int]], duration: float = 0.5):
|
|
81
|
+
"""Swipe through multiple points
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
serial (str): Android device serialno
|
|
85
|
+
points (list[tuple[int, int]]): List of (x, y) coordinates to swipe through
|
|
86
|
+
duration (float): Duration of swipe in seconds, default is 0.5
|
|
87
|
+
"""
|
|
88
|
+
async with get_device(serial) as device:
|
|
89
|
+
await to_thread.run_sync(device.swipe_points, points, duration)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@mcp.tool("drag", tags={"action:gesture"})
|
|
93
|
+
async def drag(serial: str, sx: int, sy: int, ex: int, ey: int, duration: float = 0.5):
|
|
94
|
+
"""Swipe from one point to another point.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
serial (str): Android device serialno
|
|
98
|
+
sx (int): Start X coordinate
|
|
99
|
+
sy (int): Start Y coordinate
|
|
100
|
+
ex (int): End X coordinate
|
|
101
|
+
ey (int): End Y coordinate
|
|
102
|
+
duration (float): Duration of drag in seconds, default is 0.5
|
|
103
|
+
"""
|
|
104
|
+
async with get_device(serial) as device:
|
|
105
|
+
await to_thread.run_sync(device.drag, sx, sy, ex, ey, duration)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@mcp.tool("press_key", tags={"action:key"})
|
|
109
|
+
async def press_key(serial: str, key: str):
|
|
110
|
+
"""Press a key
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
serial (str): Android device serialno
|
|
114
|
+
key (str): Key to press.
|
|
115
|
+
Supported key name includes:
|
|
116
|
+
home, back, left, right, up, down, center, menu, search, enter,
|
|
117
|
+
delete(or del), recent(recent apps), volume_up, volume_down,
|
|
118
|
+
volume_mute, camera, power
|
|
119
|
+
"""
|
|
120
|
+
async with get_device(serial) as device:
|
|
121
|
+
await to_thread.run_sync(device.press, key)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@mcp.tool("screen_on", tags={"action:screen"})
|
|
125
|
+
async def screen_on(serial: str):
|
|
126
|
+
"""Turn screen on
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
serial (str): Android device serialno
|
|
130
|
+
"""
|
|
131
|
+
async with get_device(serial) as device:
|
|
132
|
+
await to_thread.run_sync(device.screen_on)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.tool("screen_off", tags={"action:screen"})
|
|
136
|
+
async def screen_off(serial: str):
|
|
137
|
+
"""Turn screen off
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
serial (str): Android device serialno
|
|
141
|
+
"""
|
|
142
|
+
async with get_device(serial) as device:
|
|
143
|
+
await to_thread.run_sync(device.screen_off)
|