uiautomator2-mcp-server 0.1.0__py3-none-any.whl → 0.1.2__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 +82 -63
- u2mcp/_version.py +34 -0
- u2mcp/mcp.py +61 -18
- u2mcp/tools/app.py +231 -231
- u2mcp/tools/device.py +10 -7
- uiautomator2_mcp_server-0.1.2.dist-info/METADATA +113 -0
- uiautomator2_mcp_server-0.1.2.dist-info/RECORD +16 -0
- uiautomator2_mcp_server-0.1.2.dist-info/WHEEL +5 -0
- {uiautomator2_mcp_server-0.1.0.dist-info → uiautomator2_mcp_server-0.1.2.dist-info}/entry_points.txt +1 -1
- uiautomator2_mcp_server-0.1.2.dist-info/licenses/LICENSE +620 -0
- uiautomator2_mcp_server-0.1.2.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/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)
|
u2mcp/tools/device.py
CHANGED
|
@@ -3,14 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import sys
|
|
5
5
|
from base64 import b64encode
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
6
7
|
from contextlib import asynccontextmanager
|
|
7
8
|
from io import BytesIO
|
|
8
9
|
from typing import Any, Literal
|
|
9
10
|
|
|
10
11
|
import uiautomator2 as u2
|
|
11
12
|
from adbutils import adb
|
|
12
|
-
from fastmcp.dependencies import
|
|
13
|
-
from fastmcp.server.context import Context
|
|
13
|
+
from fastmcp.server.dependencies import get_context
|
|
14
14
|
from fastmcp.utilities.logging import get_logger
|
|
15
15
|
from PIL.Image import Image
|
|
16
16
|
|
|
@@ -36,7 +36,7 @@ _device_connect_lock = asyncio.Lock()
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
@asynccontextmanager
|
|
39
|
-
async def get_device(serial: str):
|
|
39
|
+
async def get_device(serial: str) -> AsyncGenerator[u2.Device]:
|
|
40
40
|
async with _device_connect_lock:
|
|
41
41
|
try:
|
|
42
42
|
semaphore, device = _devices[serial]
|
|
@@ -61,7 +61,7 @@ async def device_list() -> list[dict[str, Any]]:
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
@mcp.tool("init")
|
|
64
|
-
async def init(serial: str = ""
|
|
64
|
+
async def init(serial: str = ""):
|
|
65
65
|
"""Install essential resources to device.
|
|
66
66
|
|
|
67
67
|
Important:
|
|
@@ -110,6 +110,8 @@ async def init(serial: str = "", ctx: Context = CurrentContext()):
|
|
|
110
110
|
|
|
111
111
|
logger.info("read uiautomator2 init command stdio")
|
|
112
112
|
|
|
113
|
+
ctx = get_context()
|
|
114
|
+
|
|
113
115
|
while True:
|
|
114
116
|
tag, line = await output_queue.get()
|
|
115
117
|
|
|
@@ -156,10 +158,10 @@ async def connect(serial: str = ""):
|
|
|
156
158
|
|
|
157
159
|
if serial := serial.strip():
|
|
158
160
|
try:
|
|
159
|
-
async with get_device(serial) as
|
|
161
|
+
async with get_device(serial) as device_1:
|
|
160
162
|
# Found, then check if it's still connected
|
|
161
163
|
try:
|
|
162
|
-
return await asyncio.to_thread(lambda:
|
|
164
|
+
return await asyncio.to_thread(lambda: device_1.device_info | device_1.info)
|
|
163
165
|
except u2.ConnectError as e:
|
|
164
166
|
# Found, but not connected, delete it
|
|
165
167
|
logger.warning("Device %s is no longer connected, delete it!", serial)
|
|
@@ -172,6 +174,8 @@ async def connect(serial: str = ""):
|
|
|
172
174
|
# make new connection here!
|
|
173
175
|
async with _device_connect_lock:
|
|
174
176
|
device = await asyncio.to_thread(u2.connect, serial)
|
|
177
|
+
if device is None:
|
|
178
|
+
raise RuntimeError("Cannot connect to device")
|
|
175
179
|
logger.info("Connected to device %s", device.serial)
|
|
176
180
|
result = await asyncio.to_thread(lambda: device.device_info | device.info)
|
|
177
181
|
_devices[device.serial] = asyncio.Semaphore(), device
|
|
@@ -287,4 +291,3 @@ async def info(serial: str) -> dict[str, Any]:
|
|
|
287
291
|
|
|
288
292
|
async with get_device(serial) as device:
|
|
289
293
|
return await asyncio.to_thread(lambda: device.info)
|
|
290
|
-
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uiautomator2-mcp-server
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: uiautomator2 mcp server
|
|
5
|
+
Author-email: tanbro <tanbro@163.com>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: homepage, https://github.com/tanbro/uiautomator2-mcp-server
|
|
8
|
+
Project-URL: documentation, https://github.com/tanbro/uiautomator2-mcp-server/blob/main/README.md
|
|
9
|
+
Project-URL: repository, https://github.com/tanbro/uiautomator2-mcp-server.git
|
|
10
|
+
Project-URL: issues, https://github.com/tanbro/uiautomator2-mcp-server/issues
|
|
11
|
+
Project-URL: changelog, https://github.com/tanbro/uiautomator2-mcp-server/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: uiautomator2,mcp,fastmcp,adb,android,adbutils
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: fastmcp<3.0,>=2.11.0
|
|
23
|
+
Requires-Dist: uiautomator2<4.0,>=3.5
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# uiautomator2-mcp-server
|
|
27
|
+
|
|
28
|
+
[](https://github.com/tanbro/uiautomator2-mcp-server)
|
|
29
|
+
[](https://pypi.org/project/uiautomator2-mcp-server/)
|
|
30
|
+
|
|
31
|
+
A MCP (Model Context Protocol) server that provides tools for controlling and interacting with Android devices using uiautomator2. This server allows you to perform various operations on Android devices such as connecting to devices, taking screenshots, getting device information, accessing UI hierarchy, tap on screens, and more.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Device Management**: List connected devices, initialize devices, connect/disconnect from devices
|
|
36
|
+
- **Screen Operations**: Take screenshots, get device window size, dump UI hierarchy
|
|
37
|
+
- **Touch Actions**: Click, long click, double click at specific coordinates
|
|
38
|
+
- **Gesture Controls**: Swipe, swipe through multiple points, drag operations
|
|
39
|
+
- **System Controls**: Screen on/off, key presses
|
|
40
|
+
- **App Management**: Install, uninstall, start, stop, clear, and manage Android applications
|
|
41
|
+
- **Text Operations**: Send text input, clear text fields
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Python >= 3.11
|
|
46
|
+
- adb executable in your PATH
|
|
47
|
+
- Android device connected in debug mode
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Install the package
|
|
53
|
+
pip install uiautomator2-mcp-server
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
### Running the Server
|
|
59
|
+
|
|
60
|
+
The server can be run in different transport modes:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Run in streamable HTTP mode
|
|
64
|
+
u2mcp --host 0.0.0.0 --port 8000 --no-token http
|
|
65
|
+
|
|
66
|
+
# Run in stdio mode
|
|
67
|
+
u2mcp stdio
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Using the Tools
|
|
71
|
+
|
|
72
|
+
Connect it to any tool that supports MCP protocol.
|
|
73
|
+
|
|
74
|
+
## Available Tools
|
|
75
|
+
|
|
76
|
+
### Device Management
|
|
77
|
+
- `device_list`: Get list of connected devices
|
|
78
|
+
- `init`: Install essential resources to device
|
|
79
|
+
- `connect`, `disconnect`, `disconnect_all`: Manage device connections
|
|
80
|
+
- `info`: Get device information
|
|
81
|
+
- `window_size`: Get device window size
|
|
82
|
+
- `screenshot`: Take device screenshot
|
|
83
|
+
- `dump_hierarchy`: Get UI hierarchy of device
|
|
84
|
+
|
|
85
|
+
### Action Tools
|
|
86
|
+
- `click`: Click at specific coordinates
|
|
87
|
+
- `long_click`: Long click at specific coordinates
|
|
88
|
+
- `double_click`: Double click at specific coordinates
|
|
89
|
+
- `swipe`: Swipe from one point to another
|
|
90
|
+
- `swipe_points`: Swipe through multiple points
|
|
91
|
+
- `drag`: Drag from one point to another
|
|
92
|
+
- `press_key`: Press device key
|
|
93
|
+
- `send_text`: Send text to device
|
|
94
|
+
- `clear_text`: Clear text input
|
|
95
|
+
- `screen_on`/`screen_off`: Control screen state
|
|
96
|
+
|
|
97
|
+
### App Management
|
|
98
|
+
- `app_install`: Install an app
|
|
99
|
+
- `app_uninstall`: Uninstall an app
|
|
100
|
+
- `app_uninstall_all`: Uninstall all apps
|
|
101
|
+
- `app_start`: Start an app
|
|
102
|
+
- `app_stop`: Stop an app
|
|
103
|
+
- `app_stop_all`: Stop all apps
|
|
104
|
+
- `app_clear`: Clear app data
|
|
105
|
+
- `app_info`: Get app information
|
|
106
|
+
- `app_current`: Get current app
|
|
107
|
+
- `app_list`: List installed apps
|
|
108
|
+
- `app_list_running`: List running apps
|
|
109
|
+
- `app_auto_grant_permissions`: Auto grant permissions
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
This project is licensed under the GNU General Public License v3.0 (GPL-3.0).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
u2mcp/.gitignore,sha256=mr9Izcwvjgv215xjRKhWEZ7vsyrKWhMqvWjSLHRYDjk,13
|
|
2
|
+
u2mcp/__init__.py,sha256=xsioVkbaN9sg-us_kNMeH-DojD8xM-h6Nlhde6uRHCQ,104
|
|
3
|
+
u2mcp/__main__.py,sha256=CBZpaNarWiWkg-g-BCfwFxaMdP4EqG5ckHtc-ZMMwcE,2813
|
|
4
|
+
u2mcp/_version.py,sha256=UPabfftDNarXNRt7p9IXx9_aYhCVscDiY35GxOcfT8s,738
|
|
5
|
+
u2mcp/mcp.py,sha256=jP87QaNePDt3LGzdZnlI5W8r8P-I0NrH3Tyj336ZQO8,2017
|
|
6
|
+
u2mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
u2mcp/tools/__init__.py,sha256=tKfsoF6tYQSapfGC1ybEcU4S77cipyfO5tYR6u0eAlI,66
|
|
8
|
+
u2mcp/tools/action.py,sha256=A9dCED1NiKOJpw9lBewOQ9YQvfGOkK8bEL1L9FLjCQg,5088
|
|
9
|
+
u2mcp/tools/app.py,sha256=b-cOdDtjSKC8KeUK5TTJCPRXtzvwSH74P02Cm3mS-ck,6809
|
|
10
|
+
u2mcp/tools/device.py,sha256=herrj5IQ1jEav2W6K4i7UidJ7eQsg-WJCiHJD1qYKVA,9196
|
|
11
|
+
uiautomator2_mcp_server-0.1.2.dist-info/licenses/LICENSE,sha256=ea2KuPdrBJ0Dhw1ncbc7siZDKG4cz2z-_8cSQiu4n1g,33122
|
|
12
|
+
uiautomator2_mcp_server-0.1.2.dist-info/METADATA,sha256=9BxLeh6VjVBCtTx4S6w4YMcl2Nu7aFv8lfUJZpbk44s,4251
|
|
13
|
+
uiautomator2_mcp_server-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
uiautomator2_mcp_server-0.1.2.dist-info/entry_points.txt,sha256=y7lg94G2U5_VgrDtyY8-Ne4ZClFtHo6eAs3RnShuDSI,92
|
|
15
|
+
uiautomator2_mcp_server-0.1.2.dist-info/top_level.txt,sha256=elTNF05b2GS8jjS4Haa3ESnG6xbfRjpyoSRsivT0Uks,6
|
|
16
|
+
uiautomator2_mcp_server-0.1.2.dist-info/RECORD,,
|