uiautomator2-mcp-server 0.1.3__py3-none-any.whl → 0.2.0__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 +1 -2
- u2mcp/__main__.py +168 -57
- u2mcp/background.py +19 -0
- u2mcp/health.py +67 -0
- u2mcp/helpers.py +222 -0
- u2mcp/mcp.py +111 -18
- u2mcp/middlewares.py +40 -0
- u2mcp/tools/__init__.py +4 -0
- u2mcp/tools/action.py +9 -35
- u2mcp/tools/app.py +13 -13
- u2mcp/tools/clipboard.py +35 -0
- u2mcp/tools/device.py +119 -71
- u2mcp/tools/element.py +267 -0
- u2mcp/tools/input.py +47 -0
- u2mcp/tools/misc.py +1 -1
- u2mcp/tools/scrcpy.py +142 -0
- u2mcp/{_version.py → version.py} +2 -2
- uiautomator2_mcp_server-0.2.0.dist-info/METADATA +738 -0
- uiautomator2_mcp_server-0.2.0.dist-info/RECORD +25 -0
- {uiautomator2_mcp_server-0.1.3.dist-info → uiautomator2_mcp_server-0.2.0.dist-info}/WHEEL +1 -1
- {uiautomator2_mcp_server-0.1.3.dist-info → uiautomator2_mcp_server-0.2.0.dist-info}/entry_points.txt +1 -0
- uiautomator2_mcp_server-0.2.0.dist-info/licenses/LICENSE +190 -0
- uiautomator2_mcp_server-0.1.3.dist-info/METADATA +0 -115
- uiautomator2_mcp_server-0.1.3.dist-info/RECORD +0 -17
- uiautomator2_mcp_server-0.1.3.dist-info/licenses/LICENSE +0 -620
- {uiautomator2_mcp_server-0.1.3.dist-info → uiautomator2_mcp_server-0.2.0.dist-info}/top_level.txt +0 -0
u2mcp/mcp.py
CHANGED
|
@@ -11,23 +11,31 @@ Before performing operations on a device, you need to initialize it using the in
|
|
|
11
11
|
All operations require a device serial number to identify the target device.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import fnmatch
|
|
14
17
|
import sys
|
|
15
18
|
from contextlib import asynccontextmanager
|
|
16
19
|
from functools import partial
|
|
17
20
|
from textwrap import dedent
|
|
18
21
|
from typing import Any
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
from typing import override
|
|
22
|
-
else: # qa: noqa
|
|
23
|
-
from typing_extensions import override
|
|
24
|
-
|
|
23
|
+
from anyio import create_task_group
|
|
25
24
|
from fastmcp import FastMCP
|
|
26
25
|
from fastmcp.server.auth import AccessToken, AuthProvider
|
|
27
26
|
from pydantic import AnyHttpUrl
|
|
28
27
|
from rich.console import Console
|
|
29
28
|
from rich.markdown import Markdown
|
|
30
29
|
|
|
30
|
+
from .background import set_background_task_group
|
|
31
|
+
from .helpers import print_tags
|
|
32
|
+
from .middlewares import EmptyResponseMiddleware
|
|
33
|
+
|
|
34
|
+
if sys.version_info >= (3, 12): # qa: noqa
|
|
35
|
+
from typing import override
|
|
36
|
+
else: # qa: noqa
|
|
37
|
+
from typing_extensions import override
|
|
38
|
+
|
|
31
39
|
__all__ = ["mcp", "make_mcp"]
|
|
32
40
|
|
|
33
41
|
|
|
@@ -35,21 +43,73 @@ __all__ = ["mcp", "make_mcp"]
|
|
|
35
43
|
mcp: FastMCP
|
|
36
44
|
|
|
37
45
|
|
|
46
|
+
def _parse_tags(tags: str | None) -> set[str] | None:
|
|
47
|
+
"""Parse comma-separated tags string into a set."""
|
|
48
|
+
if not tags:
|
|
49
|
+
return None
|
|
50
|
+
return {tag.strip() for tag in tags.split(",") if tag.strip()}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _expand_wildcards(tags: set[str] | None, all_available_tags: set[str] | None) -> set[str] | None:
|
|
54
|
+
"""Expand wildcard patterns in tags.
|
|
55
|
+
|
|
56
|
+
Supports:
|
|
57
|
+
- * matches any characters
|
|
58
|
+
- ? matches exactly one character
|
|
59
|
+
- device:* matches all device:* tags
|
|
60
|
+
- *:shell matches all shell tags (device:shell, etc.)
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
device:* -> device:manage, device:info, device:capture, device:shell
|
|
64
|
+
*:shell -> device:shell
|
|
65
|
+
action:to* -> action:touch, action:tool (if exists)
|
|
66
|
+
"""
|
|
67
|
+
if not tags or not all_available_tags:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
expanded = set()
|
|
71
|
+
|
|
72
|
+
for tag in tags:
|
|
73
|
+
if "*" in tag or "?" in tag:
|
|
74
|
+
# Use fnmatch for wildcard matching
|
|
75
|
+
for existing_tag in all_available_tags:
|
|
76
|
+
if fnmatch.fnmatch(existing_tag, tag):
|
|
77
|
+
expanded.add(existing_tag)
|
|
78
|
+
else:
|
|
79
|
+
# No wildcard, add as-is
|
|
80
|
+
expanded.add(tag)
|
|
81
|
+
|
|
82
|
+
return expanded if expanded else None
|
|
83
|
+
|
|
84
|
+
|
|
38
85
|
@asynccontextmanager
|
|
39
|
-
async def _lifespan(instance: FastMCP, token: str | None):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
86
|
+
async def _lifespan(instance: FastMCP, /, show_tags: bool = True, token: str | None = None):
|
|
87
|
+
console = Console(stderr=True)
|
|
88
|
+
|
|
89
|
+
# Show enabled tags and tools if requested
|
|
90
|
+
if show_tags:
|
|
91
|
+
console.print("\n[bold cyan]Enabled Tags and Tools:[/bold cyan]")
|
|
92
|
+
await print_tags(instance, console)
|
|
93
|
+
console.print("")
|
|
94
|
+
|
|
95
|
+
if token:
|
|
96
|
+
content = Markdown(
|
|
97
|
+
dedent(f"""
|
|
98
|
+
------
|
|
99
|
+
|
|
100
|
+
Server configured with **authentication token**. Connect using this token in the Authorization header:
|
|
43
101
|
|
|
44
|
-
|
|
102
|
+
`Authorization: Bearer {token}`
|
|
45
103
|
|
|
46
|
-
|
|
104
|
+
------
|
|
105
|
+
""")
|
|
106
|
+
)
|
|
107
|
+
console.print(content)
|
|
47
108
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
yield
|
|
109
|
+
# Global task group for background tasks - keeps running until server shuts down
|
|
110
|
+
async with create_task_group() as tg:
|
|
111
|
+
set_background_task_group(tg)
|
|
112
|
+
yield
|
|
53
113
|
|
|
54
114
|
|
|
55
115
|
class _SimpleTokenAuthProvider(AuthProvider):
|
|
@@ -70,10 +130,43 @@ class _SimpleTokenAuthProvider(AuthProvider):
|
|
|
70
130
|
return None
|
|
71
131
|
|
|
72
132
|
|
|
73
|
-
def make_mcp(
|
|
133
|
+
def make_mcp(
|
|
134
|
+
token: str | None = None,
|
|
135
|
+
include_tags: str | None = None,
|
|
136
|
+
exclude_tags: str | None = None,
|
|
137
|
+
show_tags: bool = False,
|
|
138
|
+
fix_empty_responses: bool = False,
|
|
139
|
+
) -> FastMCP:
|
|
74
140
|
global mcp
|
|
75
141
|
params: dict[str, Any] = dict(name="uiautomator2", instructions=__doc__)
|
|
142
|
+
lifespan_kwargs: dict[str, Any] = {"show_tags": show_tags}
|
|
76
143
|
if token:
|
|
77
|
-
|
|
144
|
+
lifespan_kwargs["token"] = token
|
|
145
|
+
params.update(lifespan=partial(_lifespan, **lifespan_kwargs), auth=_SimpleTokenAuthProvider(token=token))
|
|
146
|
+
else:
|
|
147
|
+
params.update(lifespan=partial(_lifespan, **lifespan_kwargs))
|
|
78
148
|
mcp = FastMCP(**params)
|
|
149
|
+
|
|
150
|
+
# Add middleware to fix empty responses if enabled
|
|
151
|
+
if fix_empty_responses:
|
|
152
|
+
mcp.add_middleware(EmptyResponseMiddleware())
|
|
153
|
+
|
|
154
|
+
# Import tools to register them with the MCP (needed for wildcard expansion)
|
|
155
|
+
from . import tools as _ # noqa: F401
|
|
156
|
+
|
|
157
|
+
# Collect all available tags from registered tools for wildcard expansion
|
|
158
|
+
all_tag_set: set[str] = set()
|
|
159
|
+
for tool in mcp._tool_manager._tools.values():
|
|
160
|
+
all_tag_set.update(tool.tags or [])
|
|
161
|
+
|
|
162
|
+
# Parse and expand tag filters
|
|
163
|
+
parsed_include_tags = _expand_wildcards(_parse_tags(include_tags), all_tag_set)
|
|
164
|
+
parsed_exclude_tags = _expand_wildcards(_parse_tags(exclude_tags), all_tag_set)
|
|
165
|
+
|
|
166
|
+
# Set tag filters on the MCP instance
|
|
167
|
+
if parsed_include_tags is not None:
|
|
168
|
+
mcp.include_tags = parsed_include_tags
|
|
169
|
+
if parsed_exclude_tags is not None:
|
|
170
|
+
mcp.exclude_tags = parsed_exclude_tags
|
|
171
|
+
|
|
79
172
|
return mcp
|
u2mcp/middlewares.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Middleware to fix empty responses for Zhipu AI compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
|
|
6
|
+
from fastmcp.tools.tool import ToolResult
|
|
7
|
+
from mcp.types import CallToolRequestParams, TextContent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EmptyResponseMiddleware(Middleware):
|
|
11
|
+
"""Middleware that converts empty tool responses to non-empty values.
|
|
12
|
+
|
|
13
|
+
This is needed for compatibility with ZhiPu AI model service, which doesn't handle empty responses (content: []) correctly.
|
|
14
|
+
|
|
15
|
+
When enabled, this middleware will convert empty content to "" (empty string).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
async def on_call_tool(
|
|
19
|
+
self,
|
|
20
|
+
context: MiddlewareContext[CallToolRequestParams],
|
|
21
|
+
call_next: CallNext[CallToolRequestParams, ToolResult],
|
|
22
|
+
) -> ToolResult:
|
|
23
|
+
"""Intercept tool calls and fix empty responses."""
|
|
24
|
+
result = await call_next(context)
|
|
25
|
+
|
|
26
|
+
# Convert empty content to empty string for ZhiPu compatibility
|
|
27
|
+
if isinstance(result, ToolResult):
|
|
28
|
+
if not result.content:
|
|
29
|
+
# Empty content - return empty string
|
|
30
|
+
result.content = [TextContent(type="text", text="")]
|
|
31
|
+
elif (
|
|
32
|
+
isinstance(result.content, list)
|
|
33
|
+
and len(result.content) == 1
|
|
34
|
+
and isinstance(result.content[0], dict)
|
|
35
|
+
and not result.content[0].get("text")
|
|
36
|
+
):
|
|
37
|
+
# Missing/empty text - set to empty string
|
|
38
|
+
result.content[0]["text"] = ""
|
|
39
|
+
|
|
40
|
+
return result
|
u2mcp/tools/__init__.py
CHANGED
u2mcp/tools/action.py
CHANGED
|
@@ -13,14 +13,12 @@ __all__ = (
|
|
|
13
13
|
"swipe_points",
|
|
14
14
|
"drag",
|
|
15
15
|
"press_key",
|
|
16
|
-
"send_text",
|
|
17
|
-
"clear_text",
|
|
18
16
|
"screen_on",
|
|
19
17
|
"screen_off",
|
|
20
18
|
)
|
|
21
19
|
|
|
22
20
|
|
|
23
|
-
@mcp.tool("click")
|
|
21
|
+
@mcp.tool("click", tags={"action:touch"})
|
|
24
22
|
async def click(serial: str, x: int, y: int):
|
|
25
23
|
"""Click at specific coordinates
|
|
26
24
|
|
|
@@ -33,7 +31,7 @@ async def click(serial: str, x: int, y: int):
|
|
|
33
31
|
await to_thread.run_sync(device.click, x, y)
|
|
34
32
|
|
|
35
33
|
|
|
36
|
-
@mcp.tool("long_click")
|
|
34
|
+
@mcp.tool("long_click", tags={"action:touch"})
|
|
37
35
|
async def long_click(serial: str, x: int, y: int, duration: float = 0.5):
|
|
38
36
|
"""Long click at specific coordinates
|
|
39
37
|
|
|
@@ -47,7 +45,7 @@ async def long_click(serial: str, x: int, y: int, duration: float = 0.5):
|
|
|
47
45
|
await to_thread.run_sync(device.long_click, x, y, duration)
|
|
48
46
|
|
|
49
47
|
|
|
50
|
-
@mcp.tool("double_click")
|
|
48
|
+
@mcp.tool("double_click", tags={"action:touch"})
|
|
51
49
|
async def double_click(serial: str, x: int, y: int, duration: float = 0.1):
|
|
52
50
|
"""Double click at specific coordinates
|
|
53
51
|
|
|
@@ -61,7 +59,7 @@ async def double_click(serial: str, x: int, y: int, duration: float = 0.1):
|
|
|
61
59
|
await to_thread.run_sync(device.double_click, x, y, duration)
|
|
62
60
|
|
|
63
61
|
|
|
64
|
-
@mcp.tool("swipe")
|
|
62
|
+
@mcp.tool("swipe", tags={"action:gesture"})
|
|
65
63
|
async def swipe(serial: str, fx: int, fy: int, tx: int, ty: int, duration: float = 0.0, step: int = 0):
|
|
66
64
|
"""Swipe from one point to another
|
|
67
65
|
|
|
@@ -78,7 +76,7 @@ async def swipe(serial: str, fx: int, fy: int, tx: int, ty: int, duration: float
|
|
|
78
76
|
await to_thread.run_sync(device.swipe, fx, fy, tx, ty, duration if duration > 0 else None, step if step > 0 else None)
|
|
79
77
|
|
|
80
78
|
|
|
81
|
-
@mcp.tool("swipe_points")
|
|
79
|
+
@mcp.tool("swipe_points", tags={"action:gesture"})
|
|
82
80
|
async def swipe_points(serial: str, points: list[tuple[int, int]], duration: float = 0.5):
|
|
83
81
|
"""Swipe through multiple points
|
|
84
82
|
|
|
@@ -91,7 +89,7 @@ async def swipe_points(serial: str, points: list[tuple[int, int]], duration: flo
|
|
|
91
89
|
await to_thread.run_sync(device.swipe_points, points, duration)
|
|
92
90
|
|
|
93
91
|
|
|
94
|
-
@mcp.tool("drag")
|
|
92
|
+
@mcp.tool("drag", tags={"action:gesture"})
|
|
95
93
|
async def drag(serial: str, sx: int, sy: int, ex: int, ey: int, duration: float = 0.5):
|
|
96
94
|
"""Swipe from one point to another point.
|
|
97
95
|
|
|
@@ -107,7 +105,7 @@ async def drag(serial: str, sx: int, sy: int, ex: int, ey: int, duration: float
|
|
|
107
105
|
await to_thread.run_sync(device.drag, sx, sy, ex, ey, duration)
|
|
108
106
|
|
|
109
107
|
|
|
110
|
-
@mcp.tool("press_key")
|
|
108
|
+
@mcp.tool("press_key", tags={"action:key"})
|
|
111
109
|
async def press_key(serial: str, key: str):
|
|
112
110
|
"""Press a key
|
|
113
111
|
|
|
@@ -123,31 +121,7 @@ async def press_key(serial: str, key: str):
|
|
|
123
121
|
await to_thread.run_sync(device.press, key)
|
|
124
122
|
|
|
125
123
|
|
|
126
|
-
@mcp.tool("
|
|
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")
|
|
124
|
+
@mcp.tool("screen_on", tags={"action:screen"})
|
|
151
125
|
async def screen_on(serial: str):
|
|
152
126
|
"""Turn screen on
|
|
153
127
|
|
|
@@ -158,7 +132,7 @@ async def screen_on(serial: str):
|
|
|
158
132
|
await to_thread.run_sync(device.screen_on)
|
|
159
133
|
|
|
160
134
|
|
|
161
|
-
@mcp.tool("screen_off")
|
|
135
|
+
@mcp.tool("screen_off", tags={"action:screen"})
|
|
162
136
|
async def screen_off(serial: str):
|
|
163
137
|
"""Turn screen off
|
|
164
138
|
|
u2mcp/tools/app.py
CHANGED
|
@@ -23,7 +23,7 @@ __all__ = (
|
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
@mcp.tool("app_install")
|
|
26
|
+
@mcp.tool("app_install", tags={"app:manage"})
|
|
27
27
|
async def app_install(serial: str, data: str):
|
|
28
28
|
"""Install app
|
|
29
29
|
|
|
@@ -35,7 +35,7 @@ async def app_install(serial: str, data: str):
|
|
|
35
35
|
await to_thread.run_sync(device.app_install, data)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
@mcp.tool("app_uninstall")
|
|
38
|
+
@mcp.tool("app_uninstall", tags={"app:manage"})
|
|
39
39
|
async def app_uninstall(serial: str, package_name: str) -> bool:
|
|
40
40
|
"""Uninstall an app
|
|
41
41
|
|
|
@@ -50,7 +50,7 @@ async def app_uninstall(serial: str, package_name: str) -> bool:
|
|
|
50
50
|
return await to_thread.run_sync(device.app_uninstall, package_name)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
@mcp.tool("app_uninstall_all")
|
|
53
|
+
@mcp.tool("app_uninstall_all", tags={"app:manage"})
|
|
54
54
|
async def app_uninstall_all(serial: str, excludes: list[str] | None = None) -> list[str]:
|
|
55
55
|
"""Uninstall all apps
|
|
56
56
|
|
|
@@ -65,7 +65,7 @@ async def app_uninstall_all(serial: str, excludes: list[str] | None = None) -> l
|
|
|
65
65
|
return await to_thread.run_sync(device.app_uninstall_all, excludes or [])
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
@mcp.tool("app_start")
|
|
68
|
+
@mcp.tool("app_start", tags={"app:lifecycle"})
|
|
69
69
|
async def app_start(
|
|
70
70
|
serial: str,
|
|
71
71
|
package_name: str,
|
|
@@ -86,7 +86,7 @@ async def app_start(
|
|
|
86
86
|
await to_thread.run_sync(device.app_start, package_name, activity, wait, stop)
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
@mcp.tool("app_wait")
|
|
89
|
+
@mcp.tool("app_wait", tags={"app:lifecycle"})
|
|
90
90
|
async def app_wait(serial: str, package_name: str, timeout: float = 20.0, front=False):
|
|
91
91
|
"""Wait until app launched
|
|
92
92
|
|
|
@@ -101,7 +101,7 @@ async def app_wait(serial: str, package_name: str, timeout: float = 20.0, front=
|
|
|
101
101
|
raise RuntimeError(f"Failed to wait App {package_name} to launch")
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
@mcp.tool("app_stop")
|
|
104
|
+
@mcp.tool("app_stop", tags={"app:lifecycle"})
|
|
105
105
|
async def app_stop(serial: str, package_name: str):
|
|
106
106
|
"""Stop one application
|
|
107
107
|
|
|
@@ -113,7 +113,7 @@ async def app_stop(serial: str, package_name: str):
|
|
|
113
113
|
await to_thread.run_sync(device.app_stop, package_name)
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
@mcp.tool("app_stop_all")
|
|
116
|
+
@mcp.tool("app_stop_all", tags={"app:lifecycle"})
|
|
117
117
|
async def app_stop_all(serial: str, excludes: list[str] | None = None) -> list[str]:
|
|
118
118
|
"""Stop all third party applications
|
|
119
119
|
|
|
@@ -127,7 +127,7 @@ async def app_stop_all(serial: str, excludes: list[str] | None = None) -> list[s
|
|
|
127
127
|
return await to_thread.run_sync(device.app_stop_all, excludes or [])
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
@mcp.tool("app_clear")
|
|
130
|
+
@mcp.tool("app_clear", tags={"app:config"})
|
|
131
131
|
async def app_clear(serial: str, package_name: str):
|
|
132
132
|
"""Stop and clear app data: pm clear
|
|
133
133
|
|
|
@@ -142,7 +142,7 @@ async def app_clear(serial: str, package_name: str):
|
|
|
142
142
|
await to_thread.run_sync(device.app_clear, package_name)
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
@mcp.tool("app_info")
|
|
145
|
+
@mcp.tool("app_info", tags={"app:info"})
|
|
146
146
|
async def app_info(serial: str, package_name: str) -> dict[str, Any]:
|
|
147
147
|
"""
|
|
148
148
|
Get app info
|
|
@@ -161,7 +161,7 @@ async def app_info(serial: str, package_name: str) -> dict[str, Any]:
|
|
|
161
161
|
return await to_thread.run_sync(device.app_info, package_name)
|
|
162
162
|
|
|
163
163
|
|
|
164
|
-
@mcp.tool("app_current")
|
|
164
|
+
@mcp.tool("app_current", tags={"app:info"})
|
|
165
165
|
async def app_current(serial: str) -> dict[str, Any]:
|
|
166
166
|
"""
|
|
167
167
|
Get current app info
|
|
@@ -176,7 +176,7 @@ async def app_current(serial: str) -> dict[str, Any]:
|
|
|
176
176
|
return await to_thread.run_sync(device.app_current)
|
|
177
177
|
|
|
178
178
|
|
|
179
|
-
@mcp.tool("app_list")
|
|
179
|
+
@mcp.tool("app_list", tags={"app:info"})
|
|
180
180
|
async def app_list(serial: str, filter: str = "") -> list[str]:
|
|
181
181
|
"""
|
|
182
182
|
List installed app package names
|
|
@@ -192,7 +192,7 @@ async def app_list(serial: str, filter: str = "") -> list[str]:
|
|
|
192
192
|
return await to_thread.run_sync(device.app_list, filter.strip())
|
|
193
193
|
|
|
194
194
|
|
|
195
|
-
@mcp.tool("app_list_running")
|
|
195
|
+
@mcp.tool("app_list_running", tags={"app:info"})
|
|
196
196
|
async def app_list_running(serial: str) -> list[str]:
|
|
197
197
|
"""
|
|
198
198
|
List running apps
|
|
@@ -207,7 +207,7 @@ async def app_list_running(serial: str) -> list[str]:
|
|
|
207
207
|
return await to_thread.run_sync(device.app_list_running)
|
|
208
208
|
|
|
209
209
|
|
|
210
|
-
@mcp.tool("app_auto_grant_permissions")
|
|
210
|
+
@mcp.tool("app_auto_grant_permissions", tags={"app:config"})
|
|
211
211
|
async def app_auto_grant_permissions(serial: str, package_name: str):
|
|
212
212
|
"""auto grant permissions
|
|
213
213
|
|
u2mcp/tools/clipboard.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
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__ = ("read_clipboard", "write_clipboard")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp.tool("read_clipboard", tags={"clipboard:read"})
|
|
12
|
+
async def read_clipboard(serial: str) -> str | None:
|
|
13
|
+
"""Read clipboard from device
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
serial (str): Android device serialno
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str: The actual text in the clip.
|
|
20
|
+
None: If there is no text in the clip.
|
|
21
|
+
"""
|
|
22
|
+
async with get_device(serial) as device:
|
|
23
|
+
return await to_thread.run_sync(lambda: device.clipboard)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@mcp.tool("write_clipboard", tags={"clipboard:write"})
|
|
27
|
+
async def write_clipboard(serial: str, text: str):
|
|
28
|
+
"""Write clipboard to device
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
serial (str): Android device serialno
|
|
32
|
+
text: The actual text in the clip.
|
|
33
|
+
"""
|
|
34
|
+
async with get_device(serial) as device:
|
|
35
|
+
await to_thread.run_sync(device.set_clipboard, text)
|