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 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,82 @@
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 logging
4
+ import re
5
+ import secrets
6
+ from typing import Annotated, Any, Literal
7
+
8
+ import typer
9
+
10
+
11
+ def run(
12
+ transport: Annotated[
13
+ Literal["http", "stdio"], typer.Argument(help="Run mcp server on streamable-http http or stdio transport")
14
+ ] = "stdio",
15
+ host: Annotated[
16
+ str, typer.Option("--host", "-H", show_default=False, help="Host address of streamable-http transport")
17
+ ] = "127.0.0.1",
18
+ port: Annotated[
19
+ int, typer.Option("--port", "-p", show_default=False, help="Port number of streamable-http transport")
20
+ ] = 8000,
21
+ json_response: Annotated[bool, typer.Option("--json-response", "-j", help="Whether to use JSON response format")] = True,
22
+ log_level: Annotated[
23
+ Literal["debug", "info", "warning", "error", "critical"], typer.Option("--log-level", "-l", help="Log level")
24
+ ] = "info",
25
+ no_token: Annotated[
26
+ bool,
27
+ typer.Option(
28
+ "--no-token",
29
+ help="Disable authentication bearer token verification of streamable-http transport. If not set, a token will be generated randomly.",
30
+ ),
31
+ ] = False,
32
+ token: Annotated[
33
+ str | None,
34
+ typer.Option("--token", "-t", help="Explicit set token of streamable-http authentication"),
35
+ ] = None,
36
+ ):
37
+ """Run uiautomator2 mcp server"""
38
+ logging.basicConfig(
39
+ level=log_level.upper(),
40
+ format="[%(asctime)s] %(levelname)s %(name)s - %(message)s",
41
+ handlers=[logging.StreamHandler()],
42
+ force=True,
43
+ )
44
+
45
+ logging.getLogger("mcp.server").setLevel(logging.WARNING)
46
+ logging.getLogger("sse_starlette").setLevel(logging.WARNING)
47
+ logging.getLogger("docket").setLevel(logging.WARNING)
48
+ logging.getLogger("fakeredis").setLevel(logging.WARNING)
49
+
50
+ from . import tools as _
51
+ from .mcp import mcp, update_params
52
+
53
+ transport_kwargs: dict[str, Any] = {"json_response": json_response}
54
+
55
+ update_params(transport=transport)
56
+
57
+ if transport == "http":
58
+ if token:
59
+ token = token.strip()
60
+ if not re.match(r"^[a-zA-Z0-9\-_.~!$&'()*+,;=:@]{8,64}$", token):
61
+ raise typer.BadParameter("Token must be 8-64 characters long and can only contain URL-safe characters")
62
+ elif not no_token:
63
+ token = secrets.token_urlsafe()
64
+ if token:
65
+ update_params(token=token, host=host, port=port)
66
+
67
+ if host:
68
+ transport_kwargs["host"] = host
69
+ if port:
70
+ transport_kwargs["port"] = port
71
+
72
+ mcp.run(transport="streamable-http", **transport_kwargs, log_level=log_level)
73
+ else:
74
+ mcp.run(log_level=log_level)
75
+
76
+
77
+ def main():
78
+ typer.run(run)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ 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.2'
32
+ __version_tuple__ = version_tuple = (0, 1, 2)
33
+
34
+ __commit_id__ = commit_id = None
u2mcp/mcp.py CHANGED
@@ -1,18 +1,61 @@
1
- """
2
- This MCP server provides tools for controlling and interacting with Android devices using uiautomator2.
3
-
4
- It allows you to perform various operations on Android devices such as connecting to devices, taking screenshots,
5
- getting device information, accessing UI hierarchy, tap on screens, and more...
6
-
7
- It also provides tools for managing Android applications, such as installing, uninstalling, starting, stopping, and clearing applications.
8
-
9
- Before performing operations on a device, you need to initialize it using the init tool.
10
-
11
- All operations require a device serial number to identify the target device.
12
- """
13
-
14
- from 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 contextlib import asynccontextmanager
15
+ from textwrap import dedent
16
+ from typing import Any
17
+
18
+ from fastmcp import FastMCP
19
+ from fastmcp.server.auth import AccessToken, AuthProvider
20
+ from rich.console import Console
21
+ from rich.markdown import Markdown
22
+
23
+ __all__ = ["mcp"]
24
+
25
+ _params: dict[str, Any] = {}
26
+
27
+
28
+ def update_params(**kwargs):
29
+ global _params
30
+ _params.update(kwargs)
31
+
32
+
33
+ @asynccontextmanager
34
+ async def _lifespan(instance: FastMCP):
35
+ if _params.get("transport") == "http" and (token := _params.get("token")):
36
+ content = dedent(f"""
37
+ ------
38
+
39
+ **Server configured with authentication token. Connect using this token in the Authorization header:**
40
+
41
+ `Authorization: Bearer {token}`
42
+
43
+ ------
44
+ """).strip()
45
+ Console().print(Markdown(content))
46
+
47
+ yield
48
+
49
+
50
+ class _SimpleTokenAuthProvider(AuthProvider):
51
+ _scopes = ["mcp:tools"]
52
+
53
+ async def verify_token(self, token: str) -> AccessToken | None:
54
+ if server_token := _params.get("token"):
55
+ if token == server_token:
56
+ return AccessToken(token=token, client_id="user", scopes=self._scopes)
57
+ return None
58
+ return AccessToken(token=token, client_id="user", scopes=self._scopes)
59
+
60
+
61
+ mcp = FastMCP(name="uiautomator2", instructions=__doc__, lifespan=_lifespan, auth=_SimpleTokenAuthProvider())