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
package/AGENTS.md ADDED
@@ -0,0 +1,327 @@
1
+ # DeviceTools
2
+
3
+ You are operating a real application on a real phone, plugged into this Mac.
4
+ There is no person watching the screen for you.
5
+
6
+ This file is the contract. It is the only document you need; the README is for
7
+ whoever set the machine up.
8
+
9
+ ## The one rule
10
+
11
+ **Read before you act, and act on what you read.**
12
+
13
+ `snapshot` is cheap and exact. Guessing where a control is — from a screenshot,
14
+ or from memory of the last screen — is the one failure that matters here. A
15
+ wrong tap is not a failed assertion. It is a thing that happened, on somebody's
16
+ phone, and every step you take afterwards reasons about a screen you are no
17
+ longer on.
18
+
19
+ When a verb refuses, it is refusing because it could not act unambiguously. The
20
+ answer is another `snapshot`, not another attempt.
21
+
22
+ ## Start here, every session
23
+
24
+ ```sh
25
+ ./scripts/doctor.sh
26
+ ```
27
+
28
+ A fixed list of checks, always the same ones in the same order,
29
+ `OK|WARN|FAIL <name> — <detail>`. Exit 0 means you can work. Run it again the
30
+ moment any verb exits **3**: that code means the connection to the device died,
31
+ and nothing you do afterwards is real until it is back.
32
+
33
+ If it fails with the driver missing, run `./scripts/setup.sh` once and doctor
34
+ again. Do not try to install the driver by hand.
35
+
36
+ Some failures are not yours to fix. Signing in to Xcode, registering the device
37
+ with Apple, trusting a certificate on the phone, and turning on Settings →
38
+ Developer → Enable UI Automation are all things a person has to do while holding
39
+ the device. When doctor says one of those, stop and ask — retrying will not
40
+ change the answer, and neither will installing anything.
41
+
42
+ ## The loop
43
+
44
+ ```
45
+ you edit code
46
+ you run xcodebuild / gradlew ← your own shell. DeviceTools knows nothing
47
+ about Xcode, Gradle or signing
48
+ app relaunch ← the app is back on its first screen
49
+ waypoint action=goto name=… ← walk back to where you were
50
+ snapshot ← what is on it, with the geometry
51
+ tap 7 snapshot=true ← act, and get the new screen in the same call
52
+ screenshot ← only when the tree cannot answer
53
+ ```
54
+
55
+ **Two things collapse that loop, and you should reach for both.**
56
+
57
+ `snapshot=true` on any action prints the screen it produced and renumbers the
58
+ uids, so the read you were about to do is already done. It costs nothing extra:
59
+ the action reads the tree anyway to tell you whether the screen moved.
60
+
61
+ `run` takes several steps in one call. What a person does as one continuous
62
+ act — type the password, hide the keyboard, press submit — is one call, not
63
+ four. The number of calls is what this loop costs, not the size of any one
64
+ answer:
65
+
66
+ ```
67
+ run steps='[{"tap":"label:Đăng nhập"},{"type":"{{ password }}"},
68
+ {"key":"hide"},{"tap":"label:Xác nhận"},{"wait":"screen:Home"}]'
69
+ vars='{"password":"…"}'
70
+ ```
71
+
72
+ It stops at the first failing step and shows you the screen it stopped on. uids
73
+ are refused inside a batch: a uid names a position on a screen that is already
74
+ two steps old by step three, so use selectors. `snapshot: true` inside a step is
75
+ a flag on that step, not a step of its own. Start a batch with
76
+ `{"expect": "screen:X"}` when you know where it should be beginning — it is a
77
+ wait with a three-second deadline, and it fails at the door instead of tapping
78
+ into the wrong screen.
79
+
80
+ `flow save <name>` keeps a batch under a name, in the project. If you have just
81
+ worked out how to log in, save it — the next session says `flow run login`.
82
+
83
+ **Name the project.** You have no working directory: whatever the host set when
84
+ it spawned this server is fixed for the whole session, and it is the DeviceTools
85
+ checkout, not the repository the person is working in. So `init` takes the
86
+ repository path once, and every verb after that takes `project=<that path>`.
87
+ Without it a saved flow lands in the checkout, where every app on the machine
88
+ shares it and nobody reviews it.
89
+
90
+ DeviceTools owns everything from **"the app is installed"** onward, and nothing
91
+ before it. Building and installing is your shell's job, and you are better at it
92
+ than a wrapper here would be.
93
+
94
+ ## Verbs
95
+
96
+ `./scripts/<verb>.sh`, or `devicetools <verb>` where the command is on PATH — the
97
+ same program, same arguments, same output, same exit codes. Use whichever
98
+ spelling the pointer in this repository gave you and do not mix them.
99
+
100
+ | Verb | Use |
101
+ | --- | --- |
102
+ | `doctor` | can this machine drive the device right now |
103
+ | `devices` | what is attached, and which one the config selects |
104
+ | `snapshot [--all] [--grep S]` | **read the screen.** The default way to see |
105
+ | `screenshot [name] [--scale N] [--full]` | an image, when the tree cannot answer |
106
+ | `measure <uid>` | one element in full: parent, neighbours, gaps, overlaps |
107
+ | `tap <uid\|selector> [--index N] [--count N]` | tap exactly one thing, or refuse. `--count` repeats it |
108
+ | `type <text>` | type into whatever has focus, and verify it landed |
109
+ | `swipe <direction>` | a gesture between two points |
110
+ | `scroll <direction> [--until S]` | scroll, optionally until something appears |
111
+ | `key hide` | put the keyboard away |
112
+ | `back` | one screen back; fails if the screen does not move |
113
+ | `wait <selector> [--gone]` | until something appears or disappears |
114
+ | `open <url> [--expect-app <id>]` | a deep link, and which app answered |
115
+ | `app <launch\|relaunch\|kill\|reset\|state>` | the app's lifecycle, by bundle id — no icon to find |
116
+ | `app install <path> [--launch]` | put a build on the device the config selects |
117
+ | `app uninstall [id]` | remove it |
118
+ | `logs [start\|stop\|status] [--network] [--grep S]` | what the app printed |
119
+ | `waypoint <mark\|goto\|list\|forget> <name>` | get back after a reinstall |
120
+ | `settings <key> <value>` | dark mode, text size, orientation |
121
+ | `permission <allow\|deny\|reset> <name>` | a system permission |
122
+ | `find <selector>` | resolve a selector without acting on it |
123
+ | `run --steps '[…]'` | **several steps in one call.** The biggest thing you can do about cost |
124
+ | `flow <save\|run\|list\|show\|delete> <name>` | a batch, kept under a name, in the project |
125
+
126
+ Every acting verb takes `--snapshot` (`snapshot=true` over MCP): it prints the
127
+ screen the action produced and renumbers the uids, in the same call.
128
+
129
+ `tree` and `controls` still exist and are what `snapshot` was built from. Prefer
130
+ `snapshot`: it carries the geometry and assigns the numbers everything else
131
+ takes.
132
+
133
+ ## Reading a screen
134
+
135
+ ```
136
+ $ snapshot
137
+ SCREEN "Liên hệ" #9b41 428x926 portrait
138
+
139
+ [1] Application "DemoApp" (0,0) 428x926
140
+ [2] NavigationBar #Liên hệ (0,47) 428x101
141
+ [3] Button (20,51) 36x36 enabled
142
+ [4] StaticText "Liên hệ" (19,108) 116x31
143
+
144
+ [10] TextField "Tìm kiếm" (72,368) 316x25 enabled
145
+
146
+ ⚠ hit area below the 44pt minimum — [3] [10] [19] [23] [27] [31] [35] [39]
147
+ ⚠ no identifier: a selector has to use the label, which changes with the copy — [3] [10] [19] [23] [27] [31] [35] [39]
148
+ ⚠ partly off screen by 8pt — [19] [23] [27] [31] [35] [39]
149
+ ```
150
+
151
+ **The first line identifies the screen.** `#9b41` is a short hash over the set
152
+ of (type, label) pairs on it. It is not a URL and not a class name — XCUITest
153
+ and UiAutomator publish neither — but it is stable while the screen is at rest,
154
+ which is what change detection needs.
155
+
156
+ **The number in brackets is a uid, and it dies at the next `snapshot`.** Acting
157
+ on one re-reads the screen and requires that element to still be exactly what
158
+ was numbered: same type, same identifier, same label, same value, same rectangle
159
+ give or take two points. If it is not, the action is refused and says what
160
+ changed. Nothing is scored, and there is no runner-up.
161
+
162
+ **Warnings are grouped by fault, not printed under each element.** A list of six
163
+ identical rows would otherwise report the same thing six times, and an agent
164
+ that reads fifty warnings per screen stops reading warnings. To find out which
165
+ sibling `[10]` overlaps, ask `measure 10` — that is what it is for.
166
+
167
+ The whole list of warnings, and nothing beyond it:
168
+
169
+ | Warning | Computed from |
170
+ | --- | --- |
171
+ | `overlaps a sibling by Xpt` | two sibling rectangles intersect |
172
+ | `outside its parent by Xpt` | a child's rect is not contained by its parent's |
173
+ | `off screen` / `partly off screen by Xpt` | the rect against the window size |
174
+ | `zero size, but it carries text` | width or height is 0 on an element with text |
175
+ | `hit area below the Npt minimum` | 44pt on iOS, 48dp on Android |
176
+ | `the OS truncated this text with an ellipsis` | the string ends in `…` or `...` |
177
+
178
+ Nothing at or below **two points** is reported at all. Both drivers hand back
179
+ floating-point rectangles and this pipeline floors them, so any pair of edges
180
+ carries up to two points of error; without that floor, every pair of stacked
181
+ text lines "overlaps by 1pt". Measured on a real screen: sixty-three warnings
182
+ became eight, and the eight were true.
183
+
184
+ **There is no "this text is clipped, it needs 244pt".** That needs font metrics,
185
+ which neither driver exposes. Guessing a string's width from its length would be
186
+ wrong often enough that you would learn to skip the warnings, and a signal you
187
+ skip is worse than one that was never printed. When the OS truncates a string it
188
+ leaves an ellipsis, and that is what gets reported.
189
+
190
+ ## Looking at a screen
191
+
192
+ ```sh
193
+ screenshot # longest edge 768px, about 360 tokens
194
+ screenshot --full # device resolution, about 1500
195
+ ```
196
+
197
+ Use it for what the tree cannot say: a wrong icon, a blurry asset, a colour, a
198
+ contrast, a gradient. Neither driver reports any of those, so looking is the
199
+ only way to know.
200
+
201
+ For anything about position, size or text, `snapshot` is cheaper and exact.
202
+ **The normal loop contains no images at all.**
203
+
204
+ Two things to know before you read one. Apps that mark secure inputs render them
205
+ as blank regions in any captured image — the pixels are genuinely absent, and a
206
+ blank field is not evidence it failed to draw; the verb warns when a text input
207
+ is on screen. And the image leaves this machine: it goes to your model provider,
208
+ with no masking. That is a deliberate trade for an internal tool on a
209
+ developer's own device, and it is written here rather than left to be
210
+ discovered.
211
+
212
+ ## Selectors
213
+
214
+ | Spelling | Means |
215
+ | --- | --- |
216
+ | `7` | uid — the seventh thing the last snapshot showed |
217
+ | `id:<s>` | iOS accessibilityIdentifier, Android resource-id |
218
+ | `label:<s>` | iOS accessibility label, Android content-desc |
219
+ | `text:<s>` | iOS value, Android text |
220
+ | `xy:<x>,<y>` | a raw point, checked against nothing |
221
+ | anything else | a plain string, matched against id, label and value in turn |
222
+
223
+ The first kind that matches wins — not the union of all three. An identifier
224
+ that matches exactly is a better answer than two labels that also do, and
225
+ merging them would manufacture an ambiguity the screen does not have.
226
+
227
+ `id:`, `label:` and `text:` must resolve to **exactly one** visible element
228
+ unless `--index` says which. Zero matches, or several without an index, and the
229
+ verb exits non-zero naming what it saw, without touching anything.
230
+
231
+ `kind:<Type>` matches the element's class — the word `snapshot` prints first on
232
+ the line. It is what to use for a control that has no identifier and no label,
233
+ usually with `--index`, instead of falling back to `xy:`. Coordinates break: on
234
+ this app, hiding the keyboard moved a sheet from y=507 to y=774 and every
235
+ recorded tap in it missed.
236
+
237
+ **Every selector matches whole.** `text:OTP` does not find a label reading
238
+ "Vui lòng nhập mã OTP…"; `snapshot --grep OTP` does. Reading may guess, acting
239
+ may not.
240
+
241
+ ## What the app said
242
+
243
+ ```sh
244
+ logs start # once per session
245
+ … act …
246
+ logs --grep error
247
+ logs --network
248
+ ```
249
+
250
+ Until you start a collector, actions carry no `LOG` section and `logs` has
251
+ nothing to read. That is deliberate: an action must not spawn a background
252
+ process as a side effect of touching a button, and the device under test
253
+ produced twenty-one thousand log lines in thirty seconds.
254
+
255
+ `--network` is the Network panel, **filtered from the app's own log output and
256
+ not from a proxy**. A proxy needs a CA certificate on the device, and an app
257
+ that pins its certificates defeats one. So this shows what the app chose to
258
+ print: no bodies, no headers it did not log, and nothing at all from an app that
259
+ prints none of it. On the app this was built against it returns nothing — an
260
+ empty result here means the app logs no traffic, not that the network was
261
+ quiet.
262
+
263
+ ## After a rebuild
264
+
265
+ Installing a new build puts the app back on its first screen. Every action that
266
+ lands is appended to `.state/session.jsonl`, so:
267
+
268
+ ```sh
269
+ waypoint mark otp # name where you are
270
+ … rebuild, reinstall …
271
+ waypoint goto otp # relaunch and walk back
272
+ ```
273
+
274
+ `goto` replays the recorded actions, preferring the recorded identifier over the
275
+ recorded label — a label is application copy and gets rewritten, an identifier
276
+ is chosen by an engineer and rarely does. When a step no longer resolves it
277
+ **stops** and says which step and what it was looking for. It does not search,
278
+ score or improvise. Read the screen and walk the rest yourself.
279
+
280
+ ## What every action tells you
281
+
282
+ ```
283
+ TAP 7 — resolved by uid 7, verified still at 214,69 → tapped 214,69
284
+ SCREEN changed — "Trang chủ" #60a8 → "Xác thực" #abf8
285
+ LOG 2 lines
286
+ [error] AuthService: token refresh failed
287
+ ```
288
+
289
+ Three facts: what was done, whether the screen moved, and what the app said
290
+ while it moved. Not the new tree by default — attaching one to every action
291
+ would triple the cost of a loop that mostly does not need it, and "changed" or
292
+ "unchanged" already answers the usual question.
293
+
294
+ Ask for it with `--snapshot` when the next thing you would do is read the
295
+ screen, which is most of the time while you are finding your way. It is free:
296
+ the action already fetched that tree to decide whether the screen moved.
297
+
298
+ `SCREEN same — #x, contents changed` is a fourth: the same controls holding
299
+ different text. A code being entered, a counter moving, a field filling up. Not
300
+ a new screen, and not nothing.
301
+
302
+ `SCREEN unknown` means the tree could not be read on one side or the other. It
303
+ is not the same claim as "unchanged".
304
+
305
+ ## Exit codes
306
+
307
+ | Code | Meaning |
308
+ | --- | --- |
309
+ | 0 | done |
310
+ | 1 | usage error — you called it wrong |
311
+ | 2 | environment or configuration problem |
312
+ | 3 | the device is unreachable. **Run `doctor`.** Nothing after this is real |
313
+ | 4 | the command ran, and the screen did not satisfy it |
314
+
315
+ There is no 5. It used to mean "tests failed" and retired with the test runner.
316
+
317
+ Exit 4 is an ordinary outcome, not a fault: zero matches, several matches, a
318
+ condition that never held, a uid that has gone stale. Read the message. It says
319
+ what was on the screen.
320
+
321
+ ## Where things are
322
+
323
+ `DT_HOME` is the checkout: scripts, adapters, this file. `DT_ROOT` is the
324
+ project you are working in, found by walking up for a `.devicetools` marker;
325
+ `config.json`, `.state/` and `.runs/` live there. Every relative path in a
326
+ config resolves against `DT_ROOT`, and nothing you author ever resolves into
327
+ `DT_HOME`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hưng Phan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.