device-devtools-mcp 0.1.0
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.
- package/AGENTS.md +327 -0
- package/LICENSE +21 -0
- package/README.md +491 -0
- package/VERSION +1 -0
- package/bin/device-devtools-mcp.js +106 -0
- package/bin/devicetools +315 -0
- package/config.example.json +63 -0
- package/integrations/agent-pointer.sh +48 -0
- package/integrations/claude/SKILL.md +8 -0
- package/integrations/cursor/devicetools.mdc +8 -0
- package/integrations/gemini/GEMINI.md +5 -0
- package/integrations/mcp/README.md +184 -0
- package/integrations/mcp/mcp.json +10 -0
- package/integrations/mcp/reference.sh +119 -0
- package/integrations/mcp/selftest.sh +158 -0
- package/integrations/mcp/server.sh +343 -0
- package/package.json +50 -0
- package/scripts/android/app.sh +241 -0
- package/scripts/android/back.sh +73 -0
- package/scripts/android/controls.sh +56 -0
- package/scripts/android/devices.sh +77 -0
- package/scripts/android/doctor.sh +253 -0
- package/scripts/android/lib.sh +699 -0
- package/scripts/android/logs.sh +289 -0
- package/scripts/android/permission.sh +119 -0
- package/scripts/android/settings.sh +63 -0
- package/scripts/android/setup.sh +97 -0
- package/scripts/android/tree.awk +166 -0
- package/scripts/android/tree.sh +127 -0
- package/scripts/android/type.sh +191 -0
- package/scripts/common/find.sh +95 -0
- package/scripts/common/key.sh +65 -0
- package/scripts/common/lib.sh +14 -0
- package/scripts/common/measure.sh +170 -0
- package/scripts/common/open.sh +131 -0
- package/scripts/common/screenshot.sh +102 -0
- package/scripts/common/scroll.sh +177 -0
- package/scripts/common/snapshot.sh +69 -0
- package/scripts/common/swipe.sh +171 -0
- package/scripts/common/tap.sh +226 -0
- package/scripts/common/wait.sh +212 -0
- package/scripts/common/waypoint.sh +141 -0
- package/scripts/dispatch.sh +21 -0
- package/scripts/flow.sh +266 -0
- package/scripts/init.sh +101 -0
- package/scripts/ios/app.sh +404 -0
- package/scripts/ios/back.sh +95 -0
- package/scripts/ios/controls.sh +68 -0
- package/scripts/ios/devices.sh +80 -0
- package/scripts/ios/doctor.sh +386 -0
- package/scripts/ios/lib.sh +864 -0
- package/scripts/ios/logs.sh +272 -0
- package/scripts/ios/permission.sh +108 -0
- package/scripts/ios/settings.sh +76 -0
- package/scripts/ios/setup.sh +175 -0
- package/scripts/ios/tree.sh +128 -0
- package/scripts/ios/type.sh +178 -0
- package/scripts/lib.sh +1032 -0
- package/scripts/links.tsv +44 -0
- package/scripts/relink.sh +121 -0
- package/scripts/run.sh +415 -0
- package/scripts/selftest.sh +1709 -0
- package/scripts/snapshot.awk +362 -0
- package/scripts/verify-npm-package.js +133 -0
- package/tests/fixtures/ios-contacts-list.expected +52 -0
- package/tests/fixtures/ios-contacts-list.rows +140 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# doctor.sh — is this machine able to drive the app right now?
|
|
3
|
+
#
|
|
4
|
+
# doctor.sh [--start-ua2]
|
|
5
|
+
#
|
|
6
|
+
# Eight checks, always the same eight, always in this order, one line each:
|
|
7
|
+
#
|
|
8
|
+
# OK|WARN|FAIL <name> — <detail>
|
|
9
|
+
#
|
|
10
|
+
# The order is the dependency order, so the first FAIL is the thing to fix; the
|
|
11
|
+
# checks below it are reported anyway rather than skipped, because "we stopped
|
|
12
|
+
# looking" and "we looked and it was fine" must not print the same way.
|
|
13
|
+
#
|
|
14
|
+
# --start-ua2 restarts a UiAutomator2 server that is installed but not running.
|
|
15
|
+
# Without it this script only reports; it changes nothing. --recover is the same
|
|
16
|
+
# flag under the platform-neutral name every other script uses when it tells you
|
|
17
|
+
# to run this.
|
|
18
|
+
#
|
|
19
|
+
# exit 0 everything needed is present (WARN is allowed)
|
|
20
|
+
# exit 2 at least one FAIL
|
|
21
|
+
|
|
22
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
23
|
+
|
|
24
|
+
START_UA2=0
|
|
25
|
+
while [ $# -gt 0 ]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--start-ua2) START_UA2=1; shift ;;
|
|
28
|
+
# --recover is the platform-neutral spelling, so that any caller can ask any
|
|
29
|
+
# adapter to put itself right without knowing what "right" involves here.
|
|
30
|
+
--recover) START_UA2=1; shift ;;
|
|
31
|
+
-h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
32
|
+
*) die "unknown argument: $1 (usage: doctor.sh [--start-ua2|--recover])" 1 ;;
|
|
33
|
+
esac
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
FAILED=0
|
|
37
|
+
# The status word is the whole point of these nine lines, so it is the only
|
|
38
|
+
# thing tinted. The check names stay plain: they are a fixed list in a fixed
|
|
39
|
+
# order and colouring them would suggest one matters more than another.
|
|
40
|
+
say() {
|
|
41
|
+
local col=""
|
|
42
|
+
case "$1" in OK) col="$C_PASS" ;; WARN) col="$C_NOTE" ;; FAIL) col="$C_FAIL" ;; esac
|
|
43
|
+
printf '%s%s%s %s — %s\n' "$col" "$1" "${col:+$C_OFF}" "$2" "$3"
|
|
44
|
+
[ "$1" = FAIL ] && FAILED=1
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# --- 1. sdk ------------------------------------------------------------------
|
|
49
|
+
if command -v adb >/dev/null 2>&1; then
|
|
50
|
+
say OK sdk "adb $(adb version 2>/dev/null | awk 'NR==1{print $NF}')"
|
|
51
|
+
else
|
|
52
|
+
say FAIL sdk "adb not on PATH — install Android platform-tools, see README.md"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# --- 2. jq -------------------------------------------------------------------
|
|
56
|
+
if command -v jq >/dev/null 2>&1; then
|
|
57
|
+
say OK jq "$(jq --version 2>/dev/null)"
|
|
58
|
+
else
|
|
59
|
+
say FAIL jq "jq not on PATH — see README.md setup"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Everything below needs the config to be readable at all.
|
|
63
|
+
load_config
|
|
64
|
+
|
|
65
|
+
SERIAL="$(cfg '.device.serial' '')"
|
|
66
|
+
PORT="$(cfg '.ua2.local_port' '6790')"
|
|
67
|
+
DEVICE_PORT="$(cfg '.ua2.device_port' '6790')"
|
|
68
|
+
PKG="$(cfg '.app.package' '')"
|
|
69
|
+
|
|
70
|
+
# --- 3. device ---------------------------------------------------------------
|
|
71
|
+
DEVICE_OK=0
|
|
72
|
+
if [ -z "$SERIAL" ]; then
|
|
73
|
+
say FAIL device "device.serial is not set in $DT_CONFIG — run 'adb devices' and copy the serial"
|
|
74
|
+
elif ! command -v adb >/dev/null 2>&1; then
|
|
75
|
+
say FAIL device "cannot look for $SERIAL without adb"
|
|
76
|
+
else
|
|
77
|
+
STATE="$(adb devices 2>/dev/null | awk -v s="$SERIAL" '$1 == s { print $2; exit }')"
|
|
78
|
+
case "$STATE" in
|
|
79
|
+
device)
|
|
80
|
+
DEVICE_OK=1
|
|
81
|
+
REL="$(adb_dev shell getprop ro.build.version.release 2>/dev/null | tr -d '\r')"
|
|
82
|
+
say OK device "$SERIAL — Android ${REL:-?} ($(android_kind))" ;;
|
|
83
|
+
unauthorized)
|
|
84
|
+
say FAIL device "$SERIAL is unauthorized — accept the USB debugging prompt on the device" ;;
|
|
85
|
+
offline)
|
|
86
|
+
say FAIL device "$SERIAL is offline — reconnect it, or run 'adb kill-server'" ;;
|
|
87
|
+
"")
|
|
88
|
+
ATTACHED="$(adb devices 2>/dev/null | awk 'NR>1 && $1 != "" { printf "%s ", $1 }')"
|
|
89
|
+
if [ -z "$ATTACHED" ]; then
|
|
90
|
+
say FAIL device "$SERIAL not attached, and no device is — plug one in or start the emulator"
|
|
91
|
+
else
|
|
92
|
+
say FAIL device "$SERIAL not attached; attached: ${ATTACHED% }"
|
|
93
|
+
fi ;;
|
|
94
|
+
*)
|
|
95
|
+
say FAIL device "$SERIAL is in state '$STATE'" ;;
|
|
96
|
+
esac
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# --- 4. ua2-apk --------------------------------------------------------------
|
|
100
|
+
#
|
|
101
|
+
# Two packages, and both matter: the server and the instrumentation that starts
|
|
102
|
+
# it. Reporting only the first turns "the test runner APK is missing" into the
|
|
103
|
+
# much less useful "the server will not start".
|
|
104
|
+
UA2_PKG=io.appium.uiautomator2.server
|
|
105
|
+
UA2_TEST_PKG=io.appium.uiautomator2.server.test
|
|
106
|
+
APK_OK=0
|
|
107
|
+
if [ "$DEVICE_OK" -eq 0 ]; then
|
|
108
|
+
say FAIL ua2-apk "cannot check without a working device connection"
|
|
109
|
+
else
|
|
110
|
+
HAVE_SRV=0; HAVE_TEST=0
|
|
111
|
+
pkg_installed "$UA2_PKG" && HAVE_SRV=1
|
|
112
|
+
pkg_installed "$UA2_TEST_PKG" && HAVE_TEST=1
|
|
113
|
+
if [ "$HAVE_SRV" -eq 1 ] && [ "$HAVE_TEST" -eq 1 ]; then
|
|
114
|
+
APK_OK=1
|
|
115
|
+
VER="$(adb_dev shell dumpsys package "$UA2_PKG" 2>/dev/null | awk -F= '/versionName/ { print $2; exit }' | tr -d '\r')"
|
|
116
|
+
say OK ua2-apk "UiAutomator2 server ${VER:-installed}"
|
|
117
|
+
elif [ "$HAVE_SRV" -eq 1 ]; then
|
|
118
|
+
say FAIL ua2-apk "$UA2_TEST_PKG missing — both packages are needed; run: scripts/setup.sh"
|
|
119
|
+
else
|
|
120
|
+
say FAIL ua2-apk "$UA2_PKG not installed — run: scripts/setup.sh"
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# --- 5. forward --------------------------------------------------------------
|
|
125
|
+
#
|
|
126
|
+
# Idempotent by construction: `adb forward` replaces an existing mapping for the
|
|
127
|
+
# same local port rather than adding a second one, so establishing it here costs
|
|
128
|
+
# nothing when it is already there and removes a whole class of "it worked
|
|
129
|
+
# yesterday" from the user's day.
|
|
130
|
+
#
|
|
131
|
+
# TAKING A PORT THAT BELONGS TO ANOTHER DEVICE IS NOT RECOVERY, IT IS THEFT.
|
|
132
|
+
#
|
|
133
|
+
# Because the mapping is replaced rather than added, a second config naming the
|
|
134
|
+
# same local port silently redirects the first one's traffic to this device. It
|
|
135
|
+
# happened here with an emulator and a phone: the emulator's suite went on
|
|
136
|
+
# running, reading a Huawei's screen, and failed with `expected 'Clock', found
|
|
137
|
+
# 'Chỉnh sửa báo thức'` — a real device answering a question about a different
|
|
138
|
+
# one. On a fleet that is a green run against hardware nobody tested.
|
|
139
|
+
#
|
|
140
|
+
# So the port is only taken when it is free or already ours.
|
|
141
|
+
OWNER=""
|
|
142
|
+
if [ "$DEVICE_OK" -eq 1 ]; then
|
|
143
|
+
OWNER="$(adb forward --list 2>/dev/null \
|
|
144
|
+
| awk -v p="tcp:$PORT" '$2 == p { print $1; exit }')"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
FORWARD_OK=0
|
|
148
|
+
if [ "$DEVICE_OK" -eq 0 ]; then
|
|
149
|
+
say FAIL forward "cannot forward without a working device connection"
|
|
150
|
+
elif [ -n "$OWNER" ] && [ "$OWNER" != "$(device_serial)" ]; then
|
|
151
|
+
say FAIL forward "127.0.0.1:$PORT is already forwarded to $OWNER, and taking it would point this config at that device — give one of them its own ua2.local_port"
|
|
152
|
+
elif adb_dev forward "tcp:$PORT" "tcp:$DEVICE_PORT" >/dev/null 2>&1; then
|
|
153
|
+
FORWARD_OK=1
|
|
154
|
+
say OK forward "127.0.0.1:$PORT → device:$DEVICE_PORT"
|
|
155
|
+
else
|
|
156
|
+
say FAIL forward "adb forward tcp:$PORT → tcp:$DEVICE_PORT failed"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# --- 6. ua2 ------------------------------------------------------------------
|
|
160
|
+
ua2_ready() {
|
|
161
|
+
curl -sS -m 5 "http://127.0.0.1:$PORT/status" 2>/dev/null \
|
|
162
|
+
| jq -e '.value.ready == true' >/dev/null 2>&1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# A COLD START IS NOT A WARM START, AND CI IS ALWAYS COLD.
|
|
166
|
+
#
|
|
167
|
+
# Ten seconds was measured against a server that had been run before. Installing
|
|
168
|
+
# the APK first — which CI does on every job, and which a human does once —
|
|
169
|
+
# leaves the runtime with the dexopt and first-run work still to do, and it took
|
|
170
|
+
# longer than that. The check then reported a driver that was starting normally
|
|
171
|
+
# as broken, and a second identical run passed.
|
|
172
|
+
#
|
|
173
|
+
# Sixty seconds is the budget now. Nothing waits that long in the normal case:
|
|
174
|
+
# it polls, so a warm start still returns in about a second. What the number
|
|
175
|
+
# controls is only how patient the failure is.
|
|
176
|
+
UA2_START_SECONDS=60
|
|
177
|
+
|
|
178
|
+
start_ua2() {
|
|
179
|
+
# Instrumentation runs for as long as the server lives, so it is detached and
|
|
180
|
+
# its output discarded; the readiness check below is the real answer.
|
|
181
|
+
adb_dev shell am instrument -w -e disableAnalytics true \
|
|
182
|
+
"$UA2_TEST_PKG/androidx.test.runner.AndroidJUnitRunner" >/dev/null 2>&1 &
|
|
183
|
+
local i
|
|
184
|
+
i=0
|
|
185
|
+
while [ "$i" -lt "$UA2_START_SECONDS" ]; do
|
|
186
|
+
sleep 1
|
|
187
|
+
ua2_ready && return 0
|
|
188
|
+
i=$((i + 1))
|
|
189
|
+
done
|
|
190
|
+
return 1
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
UA2_OK=0
|
|
194
|
+
if [ "$FORWARD_OK" -eq 0 ]; then
|
|
195
|
+
say FAIL ua2 "cannot reach the server without a port forward"
|
|
196
|
+
elif ua2_ready; then
|
|
197
|
+
UA2_OK=1
|
|
198
|
+
BUILD="$(curl -sS -m 5 "http://127.0.0.1:$PORT/status" 2>/dev/null | jq -r '.value.build.version // "?"')"
|
|
199
|
+
say OK ua2 "responding on 127.0.0.1:$PORT, build $BUILD"
|
|
200
|
+
elif [ "$APK_OK" -eq 0 ]; then
|
|
201
|
+
say FAIL ua2 "not responding on 127.0.0.1:$PORT, and the server APK is missing"
|
|
202
|
+
elif [ "$START_UA2" -eq 1 ]; then
|
|
203
|
+
if start_ua2; then
|
|
204
|
+
UA2_OK=1
|
|
205
|
+
say OK ua2 "started, responding on 127.0.0.1:$PORT"
|
|
206
|
+
else
|
|
207
|
+
say FAIL ua2 "installed but did not become ready within ${UA2_START_SECONDS}s of starting"
|
|
208
|
+
fi
|
|
209
|
+
else
|
|
210
|
+
say FAIL ua2 "installed but not running on 127.0.0.1:$PORT — re-run with --start-ua2"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# --- 7. ua2-session ----------------------------------------------------------
|
|
214
|
+
#
|
|
215
|
+
# A server that answers /status can still refuse to create a session, and every
|
|
216
|
+
# verb needs one. Checking the thing the verbs actually depend on is the point
|
|
217
|
+
# of a doctor.
|
|
218
|
+
if [ "$UA2_OK" -eq 0 ]; then
|
|
219
|
+
say FAIL ua2-session "cannot create a session without a responding server"
|
|
220
|
+
else
|
|
221
|
+
ua2_drop_session
|
|
222
|
+
SID="$(curl -sS -m 20 -X POST "http://127.0.0.1:$PORT/session" \
|
|
223
|
+
-H 'Content-Type: application/json' \
|
|
224
|
+
-d '{"capabilities":{"alwaysMatch":{},"firstMatch":[{}]}}' 2>/dev/null \
|
|
225
|
+
| jq -r '.value.sessionId // .sessionId // empty' 2>/dev/null)"
|
|
226
|
+
if [ -n "$SID" ]; then
|
|
227
|
+
printf '%s' "$SID" > "$(session_file)"
|
|
228
|
+
N="$(curl -sS -m 10 "http://127.0.0.1:$PORT/session/$SID/source?format=xml" 2>/dev/null \
|
|
229
|
+
| jq -r '.value // empty' 2>/dev/null | awk -f "$TREE_AWK" 2>/dev/null | wc -l | tr -d ' ')"
|
|
230
|
+
say OK ua2-session "created, screen has ${N:-0} elements"
|
|
231
|
+
else
|
|
232
|
+
say FAIL ua2-session "the server would not create a session — try --start-ua2"
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# --- 8. app ------------------------------------------------------------------
|
|
237
|
+
#
|
|
238
|
+
# WARN, not FAIL: reading the screen and driving whatever is in the foreground
|
|
239
|
+
# works without the app under test being installed, and a doctor that fails
|
|
240
|
+
# for a package the user has not built yet is a doctor people stop running.
|
|
241
|
+
if [ -z "$PKG" ]; then
|
|
242
|
+
say WARN app "app.package is not set in $DT_CONFIG — lifecycle verbs will not work"
|
|
243
|
+
elif [ "$DEVICE_OK" -eq 0 ]; then
|
|
244
|
+
say WARN app "cannot check $PKG without a working device connection"
|
|
245
|
+
elif pkg_installed "$PKG"; then
|
|
246
|
+
VER="$(adb_dev shell dumpsys package "$PKG" 2>/dev/null | awk -F= '/versionName/ { print $2; exit }' | tr -d '\r')"
|
|
247
|
+
say OK app "$PKG${VER:+ $VER} installed"
|
|
248
|
+
else
|
|
249
|
+
say WARN app "$PKG is not installed — run $(as_cmd app install)"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
[ "$FAILED" -eq 0 ] || exit 2
|
|
253
|
+
exit 0
|