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.
Files changed (66) hide show
  1. package/AGENTS.md +327 -0
  2. package/LICENSE +21 -0
  3. package/README.md +491 -0
  4. package/VERSION +1 -0
  5. package/bin/device-devtools-mcp.js +106 -0
  6. package/bin/devicetools +315 -0
  7. package/config.example.json +63 -0
  8. package/integrations/agent-pointer.sh +48 -0
  9. package/integrations/claude/SKILL.md +8 -0
  10. package/integrations/cursor/devicetools.mdc +8 -0
  11. package/integrations/gemini/GEMINI.md +5 -0
  12. package/integrations/mcp/README.md +184 -0
  13. package/integrations/mcp/mcp.json +10 -0
  14. package/integrations/mcp/reference.sh +119 -0
  15. package/integrations/mcp/selftest.sh +158 -0
  16. package/integrations/mcp/server.sh +343 -0
  17. package/package.json +50 -0
  18. package/scripts/android/app.sh +241 -0
  19. package/scripts/android/back.sh +73 -0
  20. package/scripts/android/controls.sh +56 -0
  21. package/scripts/android/devices.sh +77 -0
  22. package/scripts/android/doctor.sh +253 -0
  23. package/scripts/android/lib.sh +699 -0
  24. package/scripts/android/logs.sh +289 -0
  25. package/scripts/android/permission.sh +119 -0
  26. package/scripts/android/settings.sh +63 -0
  27. package/scripts/android/setup.sh +97 -0
  28. package/scripts/android/tree.awk +166 -0
  29. package/scripts/android/tree.sh +127 -0
  30. package/scripts/android/type.sh +191 -0
  31. package/scripts/common/find.sh +95 -0
  32. package/scripts/common/key.sh +65 -0
  33. package/scripts/common/lib.sh +14 -0
  34. package/scripts/common/measure.sh +170 -0
  35. package/scripts/common/open.sh +131 -0
  36. package/scripts/common/screenshot.sh +102 -0
  37. package/scripts/common/scroll.sh +177 -0
  38. package/scripts/common/snapshot.sh +69 -0
  39. package/scripts/common/swipe.sh +171 -0
  40. package/scripts/common/tap.sh +226 -0
  41. package/scripts/common/wait.sh +212 -0
  42. package/scripts/common/waypoint.sh +141 -0
  43. package/scripts/dispatch.sh +21 -0
  44. package/scripts/flow.sh +266 -0
  45. package/scripts/init.sh +101 -0
  46. package/scripts/ios/app.sh +404 -0
  47. package/scripts/ios/back.sh +95 -0
  48. package/scripts/ios/controls.sh +68 -0
  49. package/scripts/ios/devices.sh +80 -0
  50. package/scripts/ios/doctor.sh +386 -0
  51. package/scripts/ios/lib.sh +864 -0
  52. package/scripts/ios/logs.sh +272 -0
  53. package/scripts/ios/permission.sh +108 -0
  54. package/scripts/ios/settings.sh +76 -0
  55. package/scripts/ios/setup.sh +175 -0
  56. package/scripts/ios/tree.sh +128 -0
  57. package/scripts/ios/type.sh +178 -0
  58. package/scripts/lib.sh +1032 -0
  59. package/scripts/links.tsv +44 -0
  60. package/scripts/relink.sh +121 -0
  61. package/scripts/run.sh +415 -0
  62. package/scripts/selftest.sh +1709 -0
  63. package/scripts/snapshot.awk +362 -0
  64. package/scripts/verify-npm-package.js +133 -0
  65. package/tests/fixtures/ios-contacts-list.expected +52 -0
  66. 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