uiautomator2-mcp-server 0.1.1__py3-none-any.whl → 0.1.3__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 +2 -2
- u2mcp/__main__.py +82 -60
- u2mcp/_version.py +34 -34
- u2mcp/mcp.py +79 -18
- u2mcp/tools/__init__.py +4 -3
- u2mcp/tools/action.py +169 -169
- u2mcp/tools/app.py +232 -231
- u2mcp/tools/device.py +259 -292
- u2mcp/tools/misc.py +17 -0
- {uiautomator2_mcp_server-0.1.1.dist-info → uiautomator2_mcp_server-0.1.3.dist-info}/METADATA +115 -106
- uiautomator2_mcp_server-0.1.3.dist-info/RECORD +17 -0
- {uiautomator2_mcp_server-0.1.1.dist-info → uiautomator2_mcp_server-0.1.3.dist-info}/WHEEL +1 -1
- {uiautomator2_mcp_server-0.1.1.dist-info → uiautomator2_mcp_server-0.1.3.dist-info}/licenses/LICENSE +620 -620
- uiautomator2_mcp_server-0.1.1.dist-info/RECORD +0 -16
- {uiautomator2_mcp_server-0.1.1.dist-info → uiautomator2_mcp_server-0.1.3.dist-info}/entry_points.txt +0 -0
- {uiautomator2_mcp_server-0.1.1.dist-info → uiautomator2_mcp_server-0.1.3.dist-info}/top_level.txt +0 -0
u2mcp/.gitignore
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
_version.py
|
|
1
|
+
_version.py
|
u2mcp/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
from . import _version as version
|
|
2
|
-
from ._version import __commit_id__, __version__, __version_tuple__
|
|
1
|
+
from . import _version as version
|
|
2
|
+
from ._version import __commit_id__, __version__, __version_tuple__
|
u2mcp/__main__.py
CHANGED
|
@@ -1,60 +1,82 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
if
|
|
60
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
import secrets
|
|
6
|
+
from typing import Annotated, Any, Literal
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from .mcp import make_mcp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(
|
|
14
|
+
transport: Annotated[
|
|
15
|
+
Literal["http", "stdio"], typer.Argument(help="Run mcp server on streamable-http http or stdio transport")
|
|
16
|
+
] = "stdio",
|
|
17
|
+
host: Annotated[
|
|
18
|
+
str, typer.Option("--host", "-H", show_default=False, help="Host address of streamable-http transport")
|
|
19
|
+
] = "127.0.0.1",
|
|
20
|
+
port: Annotated[
|
|
21
|
+
int, typer.Option("--port", "-p", show_default=False, help="Port number of streamable-http transport")
|
|
22
|
+
] = 8000,
|
|
23
|
+
json_response: Annotated[bool, typer.Option("--json-response", "-j", help="Whether to use JSON response format")] = True,
|
|
24
|
+
log_level: Annotated[
|
|
25
|
+
Literal["debug", "info", "warning", "error", "critical"], typer.Option("--log-level", "-l", help="Log level")
|
|
26
|
+
] = "info",
|
|
27
|
+
no_token: Annotated[
|
|
28
|
+
bool,
|
|
29
|
+
typer.Option(
|
|
30
|
+
"--no-token",
|
|
31
|
+
help="Disable authentication bearer token verification of streamable-http transport. If not set, a token will be generated randomly.",
|
|
32
|
+
),
|
|
33
|
+
] = False,
|
|
34
|
+
token: Annotated[
|
|
35
|
+
str | None,
|
|
36
|
+
typer.Option("--token", "-t", help="Explicit set token of streamable-http authentication"),
|
|
37
|
+
] = None,
|
|
38
|
+
):
|
|
39
|
+
"""Run uiautomator2 mcp server"""
|
|
40
|
+
logging.basicConfig(
|
|
41
|
+
level=log_level.upper(),
|
|
42
|
+
format="[%(asctime)s] %(levelname)s %(name)s - %(message)s",
|
|
43
|
+
handlers=[logging.StreamHandler()],
|
|
44
|
+
force=True,
|
|
45
|
+
)
|
|
46
|
+
logging.getLogger("mcp.server").setLevel(logging.WARNING)
|
|
47
|
+
logging.getLogger("sse_starlette").setLevel(logging.WARNING)
|
|
48
|
+
logging.getLogger("docket").setLevel(logging.WARNING)
|
|
49
|
+
logging.getLogger("fakeredis").setLevel(logging.WARNING)
|
|
50
|
+
|
|
51
|
+
run_kwargs: dict[str, Any] = {"json_response": json_response, "log_level": log_level}
|
|
52
|
+
|
|
53
|
+
if transport == "http":
|
|
54
|
+
run_kwargs["transport"] = "streamable-http"
|
|
55
|
+
if host:
|
|
56
|
+
run_kwargs["host"] = host
|
|
57
|
+
if port:
|
|
58
|
+
run_kwargs["port"] = port
|
|
59
|
+
if token:
|
|
60
|
+
token = token.strip()
|
|
61
|
+
if not re.match(r"^[a-zA-Z0-9\-_.~!$&'()*+,;=:@]{8,64}$", token):
|
|
62
|
+
raise typer.BadParameter("Token must be 8-64 characters long and can only contain URL-safe characters")
|
|
63
|
+
elif not no_token:
|
|
64
|
+
token = secrets.token_urlsafe()
|
|
65
|
+
mcp = make_mcp(token)
|
|
66
|
+
else:
|
|
67
|
+
run_kwargs["transport"] = "stdio"
|
|
68
|
+
mcp = make_mcp()
|
|
69
|
+
|
|
70
|
+
# can NOT import tools until mcp is crated
|
|
71
|
+
from . import tools as _
|
|
72
|
+
|
|
73
|
+
# run mcp
|
|
74
|
+
mcp.run(**run_kwargs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main():
|
|
78
|
+
typer.run(run)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|
u2mcp/_version.py
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"__version__",
|
|
6
|
-
"__version_tuple__",
|
|
7
|
-
"version",
|
|
8
|
-
"version_tuple",
|
|
9
|
-
"__commit_id__",
|
|
10
|
-
"commit_id",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
|
-
version: str
|
|
25
|
-
__version__: str
|
|
26
|
-
__version_tuple__: VERSION_TUPLE
|
|
27
|
-
version_tuple: VERSION_TUPLE
|
|
28
|
-
commit_id: COMMIT_ID
|
|
29
|
-
__commit_id__: COMMIT_ID
|
|
30
|
-
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
33
|
-
|
|
34
|
-
__commit_id__ = commit_id = None
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 3)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
u2mcp/mcp.py
CHANGED
|
@@ -1,18 +1,79 @@
|
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
import sys
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from functools import partial
|
|
17
|
+
from textwrap import dedent
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
if sys.version_info >= (3, 12): # qa: noqa
|
|
21
|
+
from typing import override
|
|
22
|
+
else: # qa: noqa
|
|
23
|
+
from typing_extensions import override
|
|
24
|
+
|
|
25
|
+
from fastmcp import FastMCP
|
|
26
|
+
from fastmcp.server.auth import AccessToken, AuthProvider
|
|
27
|
+
from pydantic import AnyHttpUrl
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
from rich.markdown import Markdown
|
|
30
|
+
|
|
31
|
+
__all__ = ["mcp", "make_mcp"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Warning: You can NOT import it unless call `make_mcp()`
|
|
35
|
+
mcp: FastMCP
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@asynccontextmanager
|
|
39
|
+
async def _lifespan(instance: FastMCP, token: str | None):
|
|
40
|
+
content = Markdown(
|
|
41
|
+
dedent(f"""
|
|
42
|
+
------
|
|
43
|
+
|
|
44
|
+
Server configured with **authentication token**. Connect using this token in the Authorization header:
|
|
45
|
+
|
|
46
|
+
`Authorization: Bearer {token}`
|
|
47
|
+
|
|
48
|
+
------
|
|
49
|
+
""")
|
|
50
|
+
)
|
|
51
|
+
Console(stderr=True).print(content)
|
|
52
|
+
yield
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _SimpleTokenAuthProvider(AuthProvider):
|
|
56
|
+
@override
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
base_url: AnyHttpUrl | str | None = None,
|
|
60
|
+
required_scopes: list[str] | None = ["mcp:tools"],
|
|
61
|
+
token: str | None = None,
|
|
62
|
+
):
|
|
63
|
+
super().__init__(base_url, required_scopes)
|
|
64
|
+
self.token = token
|
|
65
|
+
|
|
66
|
+
@override
|
|
67
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
68
|
+
if self.token == token:
|
|
69
|
+
return AccessToken(token=token, client_id="user", scopes=self.required_scopes)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def make_mcp(token: str | None = None) -> FastMCP:
|
|
74
|
+
global mcp
|
|
75
|
+
params: dict[str, Any] = dict(name="uiautomator2", instructions=__doc__)
|
|
76
|
+
if token:
|
|
77
|
+
params.update(lifespan=partial(_lifespan, token=token), auth=_SimpleTokenAuthProvider(token=token))
|
|
78
|
+
mcp = FastMCP(**params)
|
|
79
|
+
return mcp
|
u2mcp/tools/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
from .action import *
|
|
2
|
-
from .app import *
|
|
3
|
-
from .device import *
|
|
1
|
+
from .action import *
|
|
2
|
+
from .app import *
|
|
3
|
+
from .device import *
|
|
4
|
+
from .misc import *
|
u2mcp/tools/action.py
CHANGED
|
@@ -1,169 +1,169 @@
|
|
|
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
|
-
"send_text",
|
|
17
|
-
"clear_text",
|
|
18
|
-
"screen_on",
|
|
19
|
-
"screen_off",
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@mcp.tool("click")
|
|
24
|
-
async def click(serial: str, x: int, y: int):
|
|
25
|
-
"""Click at specific coordinates
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
serial (str): Android device serialno
|
|
29
|
-
x (int): X coordinate
|
|
30
|
-
y (int): Y coordinate
|
|
31
|
-
"""
|
|
32
|
-
async with get_device(serial) as device:
|
|
33
|
-
await
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@mcp.tool("long_click")
|
|
37
|
-
async def long_click(serial: str, x: int, y: int, duration: float = 0.5):
|
|
38
|
-
"""Long click at specific coordinates
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
serial (str): Android device serialno
|
|
42
|
-
x (int): X coordinate
|
|
43
|
-
y (int): Y coordinate
|
|
44
|
-
duration (float): Duration of the long click in seconds, default is 0.5
|
|
45
|
-
"""
|
|
46
|
-
async with get_device(serial) as device:
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@mcp.tool("double_click")
|
|
51
|
-
async def double_click(serial: str, x: int, y: int, duration: float = 0.1):
|
|
52
|
-
"""Double click at specific coordinates
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
serial (str): Android device serialno
|
|
56
|
-
x (int): X coordinate
|
|
57
|
-
y (int): Y coordinate
|
|
58
|
-
duration (float): Duration between clicks in seconds, default is 0.1
|
|
59
|
-
"""
|
|
60
|
-
async with get_device(serial) as device:
|
|
61
|
-
await
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@mcp.tool("swipe")
|
|
65
|
-
async def swipe(serial: str, fx: int, fy: int, tx: int, ty: int, duration: float = 0.0, step: int = 0):
|
|
66
|
-
"""Swipe from one point to another
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
serial (str): Android device serialno
|
|
70
|
-
fx (int): From position X coordinate
|
|
71
|
-
fy (int): From position Y coordinate
|
|
72
|
-
tx (int): To position X coordinate
|
|
73
|
-
ty (int): To position Y coordinate
|
|
74
|
-
duration (float): duration
|
|
75
|
-
steps: 1 steps is about 5ms, if set, duration will be ignore
|
|
76
|
-
"""
|
|
77
|
-
async with get_device(serial) as device:
|
|
78
|
-
await
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@mcp.tool("swipe_points")
|
|
82
|
-
async def swipe_points(serial: str, points: list[tuple[int, int]], duration: float = 0.5):
|
|
83
|
-
"""Swipe through multiple points
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
serial (str): Android device serialno
|
|
87
|
-
points (list[tuple[int, int]]): List of (x, y) coordinates to swipe through
|
|
88
|
-
duration (float): Duration of swipe in seconds, default is 0.5
|
|
89
|
-
"""
|
|
90
|
-
async with get_device(serial) as device:
|
|
91
|
-
await
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@mcp.tool("drag")
|
|
95
|
-
async def drag(serial: str, sx: int, sy: int, ex: int, ey: int, duration: float = 0.5):
|
|
96
|
-
"""Swipe from one point to another point.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
serial (str): Android device serialno
|
|
100
|
-
sx (int): Start X coordinate
|
|
101
|
-
sy (int): Start Y coordinate
|
|
102
|
-
ex (int): End X coordinate
|
|
103
|
-
ey (int): End Y coordinate
|
|
104
|
-
duration (float): Duration of drag in seconds, default is 0.5
|
|
105
|
-
"""
|
|
106
|
-
async with get_device(serial) as device:
|
|
107
|
-
await
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
@mcp.tool("press_key")
|
|
111
|
-
async def press_key(serial: str, key: str):
|
|
112
|
-
"""Press a key
|
|
113
|
-
|
|
114
|
-
Args:
|
|
115
|
-
serial (str): Android device serialno
|
|
116
|
-
key (str): Key to press.
|
|
117
|
-
Supported key name includes:
|
|
118
|
-
home, back, left, right, up, down, center, menu, search, enter,
|
|
119
|
-
delete(or del), recent(recent apps), volume_up, volume_down,
|
|
120
|
-
volume_mute, camera, power
|
|
121
|
-
"""
|
|
122
|
-
async with get_device(serial) as device:
|
|
123
|
-
await
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@mcp.tool("send_text")
|
|
127
|
-
async def send_text(serial: str, text: str, clear: bool = False):
|
|
128
|
-
"""Send text to the current input field
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
serial (str): Android device serialno
|
|
132
|
-
text (str): input text
|
|
133
|
-
clear: clear text before input
|
|
134
|
-
"""
|
|
135
|
-
async with get_device(serial) as device:
|
|
136
|
-
await
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@mcp.tool("clear_text")
|
|
140
|
-
async def clear_text(serial: str):
|
|
141
|
-
"""Clear text in the current input field
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
serial (str): Android device serialno
|
|
145
|
-
"""
|
|
146
|
-
async with get_device(serial) as device:
|
|
147
|
-
await
|
|
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
|
|
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
|
|
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
|
+
"send_text",
|
|
17
|
+
"clear_text",
|
|
18
|
+
"screen_on",
|
|
19
|
+
"screen_off",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@mcp.tool("click")
|
|
24
|
+
async def click(serial: str, x: int, y: int):
|
|
25
|
+
"""Click at specific coordinates
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
serial (str): Android device serialno
|
|
29
|
+
x (int): X coordinate
|
|
30
|
+
y (int): Y coordinate
|
|
31
|
+
"""
|
|
32
|
+
async with get_device(serial) as device:
|
|
33
|
+
await to_thread.run_sync(device.click, x, y)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@mcp.tool("long_click")
|
|
37
|
+
async def long_click(serial: str, x: int, y: int, duration: float = 0.5):
|
|
38
|
+
"""Long click at specific coordinates
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
serial (str): Android device serialno
|
|
42
|
+
x (int): X coordinate
|
|
43
|
+
y (int): Y coordinate
|
|
44
|
+
duration (float): Duration of the long click in seconds, default is 0.5
|
|
45
|
+
"""
|
|
46
|
+
async with get_device(serial) as device:
|
|
47
|
+
await to_thread.run_sync(device.long_click, x, y, duration)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@mcp.tool("double_click")
|
|
51
|
+
async def double_click(serial: str, x: int, y: int, duration: float = 0.1):
|
|
52
|
+
"""Double click at specific coordinates
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
serial (str): Android device serialno
|
|
56
|
+
x (int): X coordinate
|
|
57
|
+
y (int): Y coordinate
|
|
58
|
+
duration (float): Duration between clicks in seconds, default is 0.1
|
|
59
|
+
"""
|
|
60
|
+
async with get_device(serial) as device:
|
|
61
|
+
await to_thread.run_sync(device.double_click, x, y, duration)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@mcp.tool("swipe")
|
|
65
|
+
async def swipe(serial: str, fx: int, fy: int, tx: int, ty: int, duration: float = 0.0, step: int = 0):
|
|
66
|
+
"""Swipe from one point to another
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
serial (str): Android device serialno
|
|
70
|
+
fx (int): From position X coordinate
|
|
71
|
+
fy (int): From position Y coordinate
|
|
72
|
+
tx (int): To position X coordinate
|
|
73
|
+
ty (int): To position Y coordinate
|
|
74
|
+
duration (float): duration
|
|
75
|
+
steps: 1 steps is about 5ms, if set, duration will be ignore
|
|
76
|
+
"""
|
|
77
|
+
async with get_device(serial) as device:
|
|
78
|
+
await to_thread.run_sync(device.swipe, fx, fy, tx, ty, duration if duration > 0 else None, step if step > 0 else None)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@mcp.tool("swipe_points")
|
|
82
|
+
async def swipe_points(serial: str, points: list[tuple[int, int]], duration: float = 0.5):
|
|
83
|
+
"""Swipe through multiple points
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
serial (str): Android device serialno
|
|
87
|
+
points (list[tuple[int, int]]): List of (x, y) coordinates to swipe through
|
|
88
|
+
duration (float): Duration of swipe in seconds, default is 0.5
|
|
89
|
+
"""
|
|
90
|
+
async with get_device(serial) as device:
|
|
91
|
+
await to_thread.run_sync(device.swipe_points, points, duration)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@mcp.tool("drag")
|
|
95
|
+
async def drag(serial: str, sx: int, sy: int, ex: int, ey: int, duration: float = 0.5):
|
|
96
|
+
"""Swipe from one point to another point.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
serial (str): Android device serialno
|
|
100
|
+
sx (int): Start X coordinate
|
|
101
|
+
sy (int): Start Y coordinate
|
|
102
|
+
ex (int): End X coordinate
|
|
103
|
+
ey (int): End Y coordinate
|
|
104
|
+
duration (float): Duration of drag in seconds, default is 0.5
|
|
105
|
+
"""
|
|
106
|
+
async with get_device(serial) as device:
|
|
107
|
+
await to_thread.run_sync(device.drag, sx, sy, ex, ey, duration)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@mcp.tool("press_key")
|
|
111
|
+
async def press_key(serial: str, key: str):
|
|
112
|
+
"""Press a key
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
serial (str): Android device serialno
|
|
116
|
+
key (str): Key to press.
|
|
117
|
+
Supported key name includes:
|
|
118
|
+
home, back, left, right, up, down, center, menu, search, enter,
|
|
119
|
+
delete(or del), recent(recent apps), volume_up, volume_down,
|
|
120
|
+
volume_mute, camera, power
|
|
121
|
+
"""
|
|
122
|
+
async with get_device(serial) as device:
|
|
123
|
+
await to_thread.run_sync(device.press, key)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@mcp.tool("send_text")
|
|
127
|
+
async def send_text(serial: str, text: str, clear: bool = False):
|
|
128
|
+
"""Send text to the current input field
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
serial (str): Android device serialno
|
|
132
|
+
text (str): input text
|
|
133
|
+
clear: clear text before input
|
|
134
|
+
"""
|
|
135
|
+
async with get_device(serial) as device:
|
|
136
|
+
await to_thread.run_sync(device.send_keys, text, clear)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@mcp.tool("clear_text")
|
|
140
|
+
async def clear_text(serial: str):
|
|
141
|
+
"""Clear text in the current input field
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
serial (str): Android device serialno
|
|
145
|
+
"""
|
|
146
|
+
async with get_device(serial) as device:
|
|
147
|
+
await to_thread.run_sync(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 to_thread.run_sync(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 to_thread.run_sync(device.screen_off)
|