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 ADDED
@@ -0,0 +1 @@
1
+ _version.py
u2mcp/__init__.py CHANGED
@@ -0,0 +1,2 @@
1
+ from . import _version as version
2
+ from ._version import __commit_id__, __version__, __version_tuple__
u2mcp/__main__.py CHANGED
@@ -1,63 +1,60 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from enum import StrEnum
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
- class Transport(StrEnum):
23
- streamable_http = "streamable-http"
24
- stdio = "stdio"
25
- # http = "http"
26
- # sse = "sse"
27
-
28
-
29
- def run(
30
- transport: Annotated[
31
- Transport, typer.Option("--transport", "-f", help="The transport mechanisms for client-server communication")
32
- ] = Transport.streamable_http,
33
- host: Annotated[str | None, typer.Option("--host", "-H", show_default=False, help="Host address for http mode")] = None,
34
- port: Annotated[int | None, typer.Option("--port", "-p", show_default=False, help="Port number for http mode")] = None,
35
- ):
36
- """Run mcp server
37
- Args:
38
- transport (Literal["http", "stdio"]): transport type
39
- host (str | None): host
40
- port (int | None): port
41
- """
42
- from . import tools as _
43
- from .mcp import mcp
44
-
45
- if transport == Transport.stdio:
46
- mcp.run(transport.value)
47
- elif transport == Transport.streamable_http:
48
- transport_kwargs = {}
49
- if host:
50
- transport_kwargs["host"] = host
51
- if port:
52
- transport_kwargs["port"] = port
53
- mcp.run(transport.value, **transport_kwargs)
54
- else:
55
- typer.Abort(f"Unknown transport: {transport}")
56
-
57
-
58
- def main():
59
- typer.run(run)
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)