uiautomator2-mcp-server 0.1.0__py3-none-any.whl → 0.1.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.
- u2mcp/.gitignore +1 -0
- u2mcp/__init__.py +2 -0
- u2mcp/__main__.py +60 -63
- u2mcp/_version.py +34 -0
- u2mcp/mcp.py +18 -18
- u2mcp/tools/app.py +231 -231
- u2mcp/tools/device.py +6 -4
- uiautomator2_mcp_server-0.1.1.dist-info/METADATA +106 -0
- uiautomator2_mcp_server-0.1.1.dist-info/RECORD +16 -0
- uiautomator2_mcp_server-0.1.1.dist-info/WHEEL +5 -0
- {uiautomator2_mcp_server-0.1.0.dist-info → uiautomator2_mcp_server-0.1.1.dist-info}/entry_points.txt +1 -1
- uiautomator2_mcp_server-0.1.1.dist-info/licenses/LICENSE +620 -0
- uiautomator2_mcp_server-0.1.1.dist-info/top_level.txt +1 -0
- uiautomator2_mcp_server-0.1.0.dist-info/METADATA +0 -11
- uiautomator2_mcp_server-0.1.0.dist-info/RECORD +0 -12
- uiautomator2_mcp_server-0.1.0.dist-info/WHEEL +0 -4
u2mcp/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
_version.py
|
u2mcp/__init__.py
CHANGED
u2mcp/__main__.py
CHANGED
|
@@ -1,63 +1,60 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
from typing import Annotated
|
|
6
|
-
|
|
7
|
-
import typer
|
|
8
|
-
|
|
9
|
-
logging.basicConfig(
|
|
10
|
-
level=logging.INFO,
|
|
11
|
-
format="[%(asctime)s] %(levelname)s %(name)s - %(message)s",
|
|
12
|
-
handlers=[logging.StreamHandler()],
|
|
13
|
-
force=True,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
logging.getLogger("mcp.server").setLevel(logging.WARNING)
|
|
17
|
-
logging.getLogger("sse_starlette").setLevel(logging.WARNING)
|
|
18
|
-
logging.getLogger("docket").setLevel(logging.WARNING)
|
|
19
|
-
logging.getLogger("fakeredis").setLevel(logging.WARNING)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
stdio
|
|
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
|
-
if __name__ == "__main__":
|
|
63
|
-
main()
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Annotated, Any, Awaitable
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(
|
|
10
|
+
level=logging.INFO,
|
|
11
|
+
format="[%(asctime)s] %(levelname)s %(name)s - %(message)s",
|
|
12
|
+
handlers=[logging.StreamHandler()],
|
|
13
|
+
force=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logging.getLogger("mcp.server").setLevel(logging.WARNING)
|
|
17
|
+
logging.getLogger("sse_starlette").setLevel(logging.WARNING)
|
|
18
|
+
logging.getLogger("docket").setLevel(logging.WARNING)
|
|
19
|
+
logging.getLogger("fakeredis").setLevel(logging.WARNING)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(
|
|
23
|
+
http: Annotated[bool, typer.Option("--http", "-h", help="Run mcp server in streamable http mode")] = False,
|
|
24
|
+
stdio: Annotated[bool, typer.Option("--stdio", "-s", help="Run mcp server in stdio mode")] = False,
|
|
25
|
+
host: Annotated[str | None, typer.Option("--host", "-H", show_default=False, help="Host address for http mode")] = None,
|
|
26
|
+
port: Annotated[int | None, typer.Option("--port", "-p", show_default=False, help="Port number for http mode")] = None,
|
|
27
|
+
log_level: Annotated[str | None, typer.Option("--log-level", "-l", help="Log level")] = None,
|
|
28
|
+
):
|
|
29
|
+
"""Run uiautomator2 mcp server"""
|
|
30
|
+
if not http and not stdio:
|
|
31
|
+
typer.Abort("Please specify one of ‘--http’ or ‘--stdio’")
|
|
32
|
+
|
|
33
|
+
from . import tools as _
|
|
34
|
+
from .mcp import mcp
|
|
35
|
+
|
|
36
|
+
awaitables: list[Awaitable] = []
|
|
37
|
+
|
|
38
|
+
if http:
|
|
39
|
+
transport_kwargs: dict[str, Any] = {}
|
|
40
|
+
if host:
|
|
41
|
+
transport_kwargs["host"] = host
|
|
42
|
+
if port:
|
|
43
|
+
transport_kwargs["port"] = port
|
|
44
|
+
awaitables.append(mcp.run_http_async(transport="streamable-http", **transport_kwargs, log_level=log_level))
|
|
45
|
+
|
|
46
|
+
if stdio:
|
|
47
|
+
awaitables.append(mcp.run_stdio_async(log_level=log_level))
|
|
48
|
+
|
|
49
|
+
async def _run():
|
|
50
|
+
await asyncio.gather(*awaitables)
|
|
51
|
+
|
|
52
|
+
asyncio.run(_run())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
typer.run(run)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
u2mcp/_version.py
ADDED
|
@@ -0,0 +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.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
u2mcp/mcp.py
CHANGED
|
@@ -1,18 +1,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
|
-
from fastmcp import FastMCP
|
|
15
|
-
|
|
16
|
-
__all__ = ["mcp"]
|
|
17
|
-
|
|
18
|
-
mcp = FastMCP(name="uiautomator2", instructions=__doc__)
|
|
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 fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
__all__ = ["mcp"]
|
|
17
|
+
|
|
18
|
+
mcp = FastMCP(name="uiautomator2", instructions=__doc__)
|
u2mcp/tools/app.py
CHANGED
|
@@ -1,231 +1,231 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from ..mcp import mcp
|
|
7
|
-
from .device import get_device
|
|
8
|
-
|
|
9
|
-
__all__ = (
|
|
10
|
-
"app_install",
|
|
11
|
-
"app_uninstall",
|
|
12
|
-
"app_uninstall_all",
|
|
13
|
-
"app_start",
|
|
14
|
-
"app_stop",
|
|
15
|
-
"app_stop_all",
|
|
16
|
-
"app_clear",
|
|
17
|
-
"app_info",
|
|
18
|
-
"app_current",
|
|
19
|
-
"app_list",
|
|
20
|
-
"app_list_running",
|
|
21
|
-
"app_auto_grant_permissions",
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@mcp.tool("app_install")
|
|
26
|
-
async def app_install(serial: str, data: str):
|
|
27
|
-
"""Install app
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
serial (str): Android device serialno
|
|
31
|
-
data (str): APK file path or url
|
|
32
|
-
"""
|
|
33
|
-
async with get_device(serial) as device:
|
|
34
|
-
await asyncio.to_thread(device.app_install, data)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@mcp.tool("app_uninstall")
|
|
38
|
-
async def app_uninstall(serial: str, package_name: str) -> bool:
|
|
39
|
-
"""Uninstall an app
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
serial (str): Android device serialno
|
|
43
|
-
package_name (str): package name
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
bool: success
|
|
47
|
-
"""
|
|
48
|
-
async with get_device(serial) as device:
|
|
49
|
-
return await asyncio.to_thread(device.app_uninstall, package_name)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@mcp.tool("app_uninstall_all")
|
|
53
|
-
async def app_uninstall_all(serial: str, excludes: list[str] | None = None) -> list[str]:
|
|
54
|
-
"""Uninstall all apps
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
serial (str): Android device serialno
|
|
58
|
-
excludes (list[str] | None): packages that do not want to uninstall
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
list[str]: list of uninstalled apps
|
|
62
|
-
"""
|
|
63
|
-
async with get_device(serial) as device:
|
|
64
|
-
return await asyncio.to_thread(device.app_uninstall_all, excludes or [])
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@mcp.tool("app_start")
|
|
68
|
-
async def app_start(
|
|
69
|
-
serial: str,
|
|
70
|
-
package_name: str,
|
|
71
|
-
activity: str | None = None,
|
|
72
|
-
wait: bool = False,
|
|
73
|
-
stop: bool = False,
|
|
74
|
-
):
|
|
75
|
-
"""Launch application
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
serial (str): Android device serialno
|
|
79
|
-
package_name (str): package name
|
|
80
|
-
activity (str): app activity
|
|
81
|
-
stop (bool): Stop app before starting the activity. (require activity)
|
|
82
|
-
wait (bool): wait until app started. default False
|
|
83
|
-
"""
|
|
84
|
-
async with get_device(serial) as device:
|
|
85
|
-
await asyncio.to_thread(device.app_start, package_name, activity, wait, stop)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@mcp.tool("app_wait")
|
|
89
|
-
async def app_wait(serial: str, package_name: str, timeout: float = 20.0, front=False):
|
|
90
|
-
"""Wait until app launched
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
serial (str): Android device serialno
|
|
94
|
-
package_name (str): package name
|
|
95
|
-
timeout (float): maximum wait time seconds
|
|
96
|
-
front (bool): wait until app is current app
|
|
97
|
-
"""
|
|
98
|
-
async with get_device(serial) as device:
|
|
99
|
-
if not await asyncio.to_thread(device.app_wait, package_name, timeout, front):
|
|
100
|
-
raise RuntimeError(f"Failed to wait App {package_name} to launch")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
@mcp.tool("app_stop")
|
|
104
|
-
async def app_stop(serial: str, package_name: str):
|
|
105
|
-
"""Stop one application
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
serial (str): Android device serialno
|
|
109
|
-
package_name (str): package name
|
|
110
|
-
"""
|
|
111
|
-
async with get_device(serial) as device:
|
|
112
|
-
await asyncio.to_thread(device.app_stop, package_name)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@mcp.tool("app_stop_all")
|
|
116
|
-
async def app_stop_all(serial: str, excludes: list[str] | None = None) -> list[str]:
|
|
117
|
-
"""Stop all third party applications
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
excludes (list): apps that do now want to kill
|
|
121
|
-
|
|
122
|
-
Returns:
|
|
123
|
-
list[str]: a list of killed apps
|
|
124
|
-
"""
|
|
125
|
-
async with get_device(serial) as device:
|
|
126
|
-
return await asyncio.to_thread(device.app_stop_all, excludes or [])
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
@mcp.tool("app_clear")
|
|
130
|
-
async def app_clear(serial: str, package_name: str):
|
|
131
|
-
"""Stop and clear app data: pm clear
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
serial (str): Android device serialno
|
|
135
|
-
package_name (str): package name
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
bool: success
|
|
139
|
-
"""
|
|
140
|
-
async with get_device(serial) as device:
|
|
141
|
-
await asyncio.to_thread(device.app_clear, package_name)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@mcp.tool("app_info")
|
|
145
|
-
async def app_info(serial: str, package_name: str) -> dict[str, Any]:
|
|
146
|
-
"""
|
|
147
|
-
Get app info
|
|
148
|
-
|
|
149
|
-
Args:
|
|
150
|
-
serial (str): Android device serialno
|
|
151
|
-
package_name (str): package name
|
|
152
|
-
|
|
153
|
-
Returns:
|
|
154
|
-
dict[str,Any]: app info
|
|
155
|
-
|
|
156
|
-
Example:
|
|
157
|
-
{"versionName": "1.1.7", "versionCode": 1001007}
|
|
158
|
-
"""
|
|
159
|
-
async with get_device(serial) as device:
|
|
160
|
-
return await asyncio.to_thread(device.app_info, package_name)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
@mcp.tool("app_current")
|
|
164
|
-
async def app_current(serial: str) -> dict[str, Any]:
|
|
165
|
-
"""
|
|
166
|
-
Get current app info
|
|
167
|
-
|
|
168
|
-
Args:
|
|
169
|
-
serial (str): Android device serialno
|
|
170
|
-
|
|
171
|
-
Returns:
|
|
172
|
-
dict[str,Any]: running app info
|
|
173
|
-
"""
|
|
174
|
-
async with get_device(serial) as device:
|
|
175
|
-
return await asyncio.to_thread(device.app_current)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
@mcp.tool("app_list")
|
|
179
|
-
async def app_list(serial: str, filter: str = "") -> list[str]:
|
|
180
|
-
"""
|
|
181
|
-
List installed app package names
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
serial (str): Android device serialno
|
|
185
|
-
filter (str): [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER]
|
|
186
|
-
|
|
187
|
-
Returns:
|
|
188
|
-
list[str]: list of apps by filter
|
|
189
|
-
"""
|
|
190
|
-
async with get_device(serial) as device:
|
|
191
|
-
return await asyncio.to_thread(device.app_list, filter.strip())
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
@mcp.tool("app_list_running")
|
|
195
|
-
async def app_list_running(serial: str) -> list[str]:
|
|
196
|
-
"""
|
|
197
|
-
List running apps
|
|
198
|
-
|
|
199
|
-
Args:
|
|
200
|
-
serial (str): Android device serialno
|
|
201
|
-
|
|
202
|
-
Returns:
|
|
203
|
-
list[str]: list of running apps
|
|
204
|
-
"""
|
|
205
|
-
async with get_device(serial) as device:
|
|
206
|
-
return await asyncio.to_thread(device.app_list_running)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
@mcp.tool("app_auto_grant_permissions")
|
|
210
|
-
async def app_auto_grant_permissions(serial: str, package_name: str):
|
|
211
|
-
"""auto grant permissions
|
|
212
|
-
|
|
213
|
-
Args:
|
|
214
|
-
serial (str): Android device serialno
|
|
215
|
-
package_name (str): package name
|
|
216
|
-
|
|
217
|
-
Help of "adb shell pm":
|
|
218
|
-
grant [--user USER_ID] PACKAGE PERMISSION
|
|
219
|
-
revoke [--user USER_ID] PACKAGE PERMISSION
|
|
220
|
-
These commands either grant or revoke permissions to apps. The permissions
|
|
221
|
-
must be declared as used in the app's manifest, be runtime permissions
|
|
222
|
-
(protection level dangerous), and the app targeting SDK greater than Lollipop MR1 (API level 22).
|
|
223
|
-
|
|
224
|
-
Help of "Android official pm" see <https://developer.android.com/tools/adb#pm>
|
|
225
|
-
Grant a permission to an app. On devices running Android 6.0 (API level 23) and higher,
|
|
226
|
-
the permission can be any permission declared in the app manifest.
|
|
227
|
-
On devices running Android 5.1 (API level 22) and lower,
|
|
228
|
-
must be an optional permission defined by the app.
|
|
229
|
-
"""
|
|
230
|
-
async with get_device(serial) as device:
|
|
231
|
-
await asyncio.to_thread(device.app_auto_grant_permissions, package_name)
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..mcp import mcp
|
|
7
|
+
from .device import get_device
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"app_install",
|
|
11
|
+
"app_uninstall",
|
|
12
|
+
"app_uninstall_all",
|
|
13
|
+
"app_start",
|
|
14
|
+
"app_stop",
|
|
15
|
+
"app_stop_all",
|
|
16
|
+
"app_clear",
|
|
17
|
+
"app_info",
|
|
18
|
+
"app_current",
|
|
19
|
+
"app_list",
|
|
20
|
+
"app_list_running",
|
|
21
|
+
"app_auto_grant_permissions",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@mcp.tool("app_install")
|
|
26
|
+
async def app_install(serial: str, data: str):
|
|
27
|
+
"""Install app
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
serial (str): Android device serialno
|
|
31
|
+
data (str): APK file path or url
|
|
32
|
+
"""
|
|
33
|
+
async with get_device(serial) as device:
|
|
34
|
+
await asyncio.to_thread(device.app_install, data)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@mcp.tool("app_uninstall")
|
|
38
|
+
async def app_uninstall(serial: str, package_name: str) -> bool:
|
|
39
|
+
"""Uninstall an app
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
serial (str): Android device serialno
|
|
43
|
+
package_name (str): package name
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
bool: success
|
|
47
|
+
"""
|
|
48
|
+
async with get_device(serial) as device:
|
|
49
|
+
return await asyncio.to_thread(device.app_uninstall, package_name)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@mcp.tool("app_uninstall_all")
|
|
53
|
+
async def app_uninstall_all(serial: str, excludes: list[str] | None = None) -> list[str]:
|
|
54
|
+
"""Uninstall all apps
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
serial (str): Android device serialno
|
|
58
|
+
excludes (list[str] | None): packages that do not want to uninstall
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
list[str]: list of uninstalled apps
|
|
62
|
+
"""
|
|
63
|
+
async with get_device(serial) as device:
|
|
64
|
+
return await asyncio.to_thread(device.app_uninstall_all, excludes or [])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@mcp.tool("app_start")
|
|
68
|
+
async def app_start(
|
|
69
|
+
serial: str,
|
|
70
|
+
package_name: str,
|
|
71
|
+
activity: str | None = None,
|
|
72
|
+
wait: bool = False,
|
|
73
|
+
stop: bool = False,
|
|
74
|
+
):
|
|
75
|
+
"""Launch application
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
serial (str): Android device serialno
|
|
79
|
+
package_name (str): package name
|
|
80
|
+
activity (str): app activity
|
|
81
|
+
stop (bool): Stop app before starting the activity. (require activity)
|
|
82
|
+
wait (bool): wait until app started. default False
|
|
83
|
+
"""
|
|
84
|
+
async with get_device(serial) as device:
|
|
85
|
+
await asyncio.to_thread(device.app_start, package_name, activity, wait, stop)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mcp.tool("app_wait")
|
|
89
|
+
async def app_wait(serial: str, package_name: str, timeout: float = 20.0, front=False):
|
|
90
|
+
"""Wait until app launched
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
serial (str): Android device serialno
|
|
94
|
+
package_name (str): package name
|
|
95
|
+
timeout (float): maximum wait time seconds
|
|
96
|
+
front (bool): wait until app is current app
|
|
97
|
+
"""
|
|
98
|
+
async with get_device(serial) as device:
|
|
99
|
+
if not await asyncio.to_thread(device.app_wait, package_name, timeout, front):
|
|
100
|
+
raise RuntimeError(f"Failed to wait App {package_name} to launch")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@mcp.tool("app_stop")
|
|
104
|
+
async def app_stop(serial: str, package_name: str):
|
|
105
|
+
"""Stop one application
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
serial (str): Android device serialno
|
|
109
|
+
package_name (str): package name
|
|
110
|
+
"""
|
|
111
|
+
async with get_device(serial) as device:
|
|
112
|
+
await asyncio.to_thread(device.app_stop, package_name)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp.tool("app_stop_all")
|
|
116
|
+
async def app_stop_all(serial: str, excludes: list[str] | None = None) -> list[str]:
|
|
117
|
+
"""Stop all third party applications
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
excludes (list): apps that do now want to kill
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
list[str]: a list of killed apps
|
|
124
|
+
"""
|
|
125
|
+
async with get_device(serial) as device:
|
|
126
|
+
return await asyncio.to_thread(device.app_stop_all, excludes or [])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@mcp.tool("app_clear")
|
|
130
|
+
async def app_clear(serial: str, package_name: str):
|
|
131
|
+
"""Stop and clear app data: pm clear
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
serial (str): Android device serialno
|
|
135
|
+
package_name (str): package name
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
bool: success
|
|
139
|
+
"""
|
|
140
|
+
async with get_device(serial) as device:
|
|
141
|
+
await asyncio.to_thread(device.app_clear, package_name)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@mcp.tool("app_info")
|
|
145
|
+
async def app_info(serial: str, package_name: str) -> dict[str, Any]:
|
|
146
|
+
"""
|
|
147
|
+
Get app info
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
serial (str): Android device serialno
|
|
151
|
+
package_name (str): package name
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
dict[str,Any]: app info
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
{"versionName": "1.1.7", "versionCode": 1001007}
|
|
158
|
+
"""
|
|
159
|
+
async with get_device(serial) as device:
|
|
160
|
+
return await asyncio.to_thread(device.app_info, package_name)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@mcp.tool("app_current")
|
|
164
|
+
async def app_current(serial: str) -> dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
Get current app info
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
serial (str): Android device serialno
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
dict[str,Any]: running app info
|
|
173
|
+
"""
|
|
174
|
+
async with get_device(serial) as device:
|
|
175
|
+
return await asyncio.to_thread(device.app_current)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@mcp.tool("app_list")
|
|
179
|
+
async def app_list(serial: str, filter: str = "") -> list[str]:
|
|
180
|
+
"""
|
|
181
|
+
List installed app package names
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
serial (str): Android device serialno
|
|
185
|
+
filter (str): [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER]
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
list[str]: list of apps by filter
|
|
189
|
+
"""
|
|
190
|
+
async with get_device(serial) as device:
|
|
191
|
+
return await asyncio.to_thread(device.app_list, filter.strip())
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@mcp.tool("app_list_running")
|
|
195
|
+
async def app_list_running(serial: str) -> list[str]:
|
|
196
|
+
"""
|
|
197
|
+
List running apps
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
serial (str): Android device serialno
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
list[str]: list of running apps
|
|
204
|
+
"""
|
|
205
|
+
async with get_device(serial) as device:
|
|
206
|
+
return await asyncio.to_thread(device.app_list_running)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@mcp.tool("app_auto_grant_permissions")
|
|
210
|
+
async def app_auto_grant_permissions(serial: str, package_name: str):
|
|
211
|
+
"""auto grant permissions
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
serial (str): Android device serialno
|
|
215
|
+
package_name (str): package name
|
|
216
|
+
|
|
217
|
+
Help of "adb shell pm":
|
|
218
|
+
grant [--user USER_ID] PACKAGE PERMISSION
|
|
219
|
+
revoke [--user USER_ID] PACKAGE PERMISSION
|
|
220
|
+
These commands either grant or revoke permissions to apps. The permissions
|
|
221
|
+
must be declared as used in the app's manifest, be runtime permissions
|
|
222
|
+
(protection level dangerous), and the app targeting SDK greater than Lollipop MR1 (API level 22).
|
|
223
|
+
|
|
224
|
+
Help of "Android official pm" see <https://developer.android.com/tools/adb#pm>
|
|
225
|
+
Grant a permission to an app. On devices running Android 6.0 (API level 23) and higher,
|
|
226
|
+
the permission can be any permission declared in the app manifest.
|
|
227
|
+
On devices running Android 5.1 (API level 22) and lower,
|
|
228
|
+
must be an optional permission defined by the app.
|
|
229
|
+
"""
|
|
230
|
+
async with get_device(serial) as device:
|
|
231
|
+
await asyncio.to_thread(device.app_auto_grant_permissions, package_name)
|