mobile-debug-mcp 0.20.1 → 0.21.1

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 (33) hide show
  1. package/dist/interact/android.js +0 -27
  2. package/dist/interact/index.js +145 -124
  3. package/dist/interact/ios.js +0 -26
  4. package/dist/scripts/capture_ui_after_tap.mjs +43 -0
  5. package/dist/scripts/check_play_observe.mjs +18 -0
  6. package/dist/scripts/check_play_substring.mjs +38 -0
  7. package/dist/scripts/dump_ui_tree.mjs +20 -0
  8. package/dist/scripts/observe-test.mjs +32 -0
  9. package/dist/scripts/press_and_observe.mjs +90 -0
  10. package/dist/scripts/press_and_wait_ui.mjs +85 -0
  11. package/dist/scripts/test_generate_and_wait.mjs +123 -0
  12. package/dist/server.js +15 -25
  13. package/dist/system/gradle.js +4 -4
  14. package/dist/utils/android/utils.js +2 -2
  15. package/dist/utils/resolve-device.js +5 -0
  16. package/docs/CHANGELOG.md +7 -1
  17. package/docs/tools/interact.md +7 -27
  18. package/package.json +2 -2
  19. package/src/interact/android.ts +1 -32
  20. package/src/interact/index.ts +98 -78
  21. package/src/interact/ios.ts +1 -31
  22. package/src/server.ts +18 -25
  23. package/src/system/gradle.ts +4 -4
  24. package/src/utils/android/utils.ts +2 -2
  25. package/src/utils/resolve-device.ts +6 -0
  26. package/test/interact/device/run-real-test.ts +3 -19
  27. package/test/interact/unit/{observe_until.test.ts → wait_for_ui.test.ts} +6 -6
  28. package/test/observe/device/wait_for_element_real.ts +3 -80
  29. package/test/observe/unit/wait_for_element_mock.ts +2 -104
  30. package/test/observe/unit/wait_for_ui_edge_cases.test.ts +41 -0
  31. package/test/observe/unit/wait_for_ui_stability.test.ts +30 -0
  32. package/test/unit/index.ts +27 -15
  33. package/test/interact/device/observe_until_device.ts +0 -24
@@ -0,0 +1,90 @@
1
+ import { ToolsObserve } from '../observe/index.js'
2
+ import { ToolsInteract } from '../interact/index.js'
3
+
4
+ async function main() {
5
+ const bundle = 'com.ideamechanics.modul8'
6
+ const sessionId = 'press-test'
7
+ console.log('Starting log stream for', bundle)
8
+ try {
9
+ const start = await ToolsObserve.startLogStreamHandler({ platform: 'ios', packageName: bundle, sessionId })
10
+ console.log('startLogStream result:', JSON.stringify(start))
11
+ } catch (e) {
12
+ console.error('startLogStream failed:', e instanceof Error ? e.message : String(e))
13
+ }
14
+
15
+ // Try to find the Generate Session element
16
+ console.log('\nSearching for "Generate Session" UI element...')
17
+ let el = null
18
+ try {
19
+ const found = await ToolsInteract.findElementHandler({ query: 'Generate Session', exact: false, timeoutMs: 3000, platform: 'ios' })
20
+ const foundFlag = found && typeof (found).found !== 'undefined' ? found.found : false
21
+ console.log('findElementHandler:', foundFlag ? 'found' : 'not found')
22
+ if (foundFlag) {
23
+ el = found.element
24
+ console.log('Matched element:', JSON.stringify(el))
25
+ }
26
+ } catch (e) {
27
+ console.error('findElementHandler error:', e instanceof Error ? e.message : String(e))
28
+ }
29
+
30
+ // If found, tap it. Otherwise, try tapping a best-effort coordinate in center of screen
31
+ try {
32
+ if (el && el.tapCoordinates) {
33
+ console.log('Tapping matched element at', JSON.stringify(el.tapCoordinates))
34
+ await ToolsInteract.tapHandler({ platform: 'ios', x: el.tapCoordinates.x, y: el.tapCoordinates.y })
35
+ } else {
36
+ console.log('Element not found; attempting center tap as fallback')
37
+ // attempt a center tap (may be harmless)
38
+ await ToolsInteract.tapHandler({ platform: 'ios', x: 200, y: 400 })
39
+ }
40
+ } catch (e) {
41
+ console.error('Tap failed:', e instanceof Error ? e.message : String(e))
42
+ }
43
+
44
+ console.log('\nObserving until network_idle (30s timeout)')
45
+ // Start a parallel log monitor to print network-like lines as they appear
46
+ // Print all log lines originating from the app bundle for inspection
47
+ let seenIds = new Set()
48
+ let monitorActive = true
49
+ const monitor = (async () => {
50
+ while (monitorActive) {
51
+ try {
52
+ const stream = await ToolsObserve.readLogStreamHandler({ platform: 'ios', sessionId, limit: 200 })
53
+ const entries = (stream && Array.isArray(stream.entries)) ? stream.entries : []
54
+ for (const ent of entries) {
55
+ const id = JSON.stringify(ent).slice(0,200)
56
+ if (seenIds.has(id)) continue
57
+ seenIds.add(id)
58
+ const msg = ent && (ent.message || ent.msg || ent) ? (ent.message || ent.msg || ent) : ''
59
+ // Print only lines that reference the app bundle
60
+ if (String(msg).includes('Modul8') || String(msg).includes('modul8') || (ent && ent.process && String(ent.process).toLowerCase().includes('modul8'))) {
61
+ console.log('[APP LOG]', new Date().toISOString(), JSON.stringify({ message: msg }))
62
+ }
63
+ }
64
+ } catch (e) { console.error('log monitor error:', e instanceof Error ? e.message : String(e)) }
65
+ await new Promise(r => setTimeout(r, 300))
66
+ }
67
+ })()
68
+
69
+ let observeResult = null
70
+ try {
71
+ observeResult = await ToolsInteract.observeUntilHandler({ type: 'network_idle', timeoutMs: 30000, pollIntervalMs: 300, platform: 'ios', includeSnapshotOnFailure: true })
72
+ console.log('observeUntil result:')
73
+ console.log(JSON.stringify(observeResult, null, 2))
74
+ } catch (e) {
75
+ console.error('observeUntil failed:', e instanceof Error ? e.message : String(e))
76
+ }
77
+
78
+ // Stop monitor
79
+ monitorActive = false
80
+
81
+ console.log('\nStopping log stream (best-effort)')
82
+ try {
83
+ const stop = await ToolsObserve.stopLogStreamHandler({ platform: 'ios', sessionId })
84
+ console.log('stopLogStream result:', JSON.stringify(stop))
85
+ } catch (e) { console.error('stopLogStream failed:', e) }
86
+
87
+ process.exit(0)
88
+ }
89
+
90
+ main().catch(err => { console.error(err); process.exit(1) })
@@ -0,0 +1,85 @@
1
+ import { ToolsObserve } from '../observe/index.js'
2
+ import { ToolsInteract } from '../interact/index.js'
3
+
4
+ async function main() {
5
+ const bundle = 'com.ideamechanics.modul8'
6
+ const sessionId = 'press-ui-test'
7
+ console.log('Starting log stream for', bundle)
8
+ try {
9
+ const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
10
+ const pkg = bundle
11
+ const sessionOpts = { platform, packageName: pkg, sessionId }
12
+ const start = await ToolsObserve.startLogStreamHandler(sessionOpts)
13
+ console.log('startLogStream result:', JSON.stringify(start))
14
+ } catch (e) {
15
+ console.error('startLogStream failed:', e instanceof Error ? e.message : String(e))
16
+ }
17
+
18
+ console.log('\nAttempting to find and tap Generate Session...')
19
+ let el = null
20
+ try {
21
+ const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
22
+ const found = await ToolsInteract.findElementHandler({ query: 'Generate Session', exact: platform === 'android', timeoutMs: 3000, platform })
23
+ const foundFlag = found && typeof (found).found !== 'undefined' ? found.found : false
24
+ console.log('findElementHandler:', foundFlag ? 'found' : 'not found')
25
+ if (foundFlag) {
26
+ el = found.element
27
+ console.log('Matched element:', JSON.stringify(el))
28
+ }
29
+ } catch (e) { console.error('findElementHandler err:', e) }
30
+
31
+ try {
32
+ const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
33
+ const device = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
34
+
35
+ // Strict foreground check: ensure the package is in foreground before tapping
36
+ try {
37
+ const fg = await ToolsInteract.get_current_activity ? await ToolsInteract.get_current_activity({ platform, deviceId: device }) : null
38
+ console.log('Foreground activity info:', fg)
39
+ if (fg && fg.activity && !String(fg.activity).includes('modul8')) {
40
+ console.error('App is not foreground. Aborting tap.')
41
+ process.exit(2)
42
+ }
43
+ } catch (e) { console.warn('Foreground check failed:', e) }
44
+
45
+ if (el && el.tapCoordinates) {
46
+ console.log('Tapping matched element at', JSON.stringify(el.tapCoordinates))
47
+ await ToolsInteract.tapHandler({ platform, x: el.tapCoordinates.x, y: el.tapCoordinates.y, deviceId: device })
48
+ } else {
49
+ console.log('Element not found, tapping center fallback')
50
+ await ToolsInteract.tapHandler({ platform, x: 200, y: 400, deviceId: device })
51
+ }
52
+ } catch (e) { console.error('Tap failed:', e) }
53
+
54
+ console.log('\nNow waiting for "Generating session" to disappear (timeout 30s)')
55
+ const start = Date.now()
56
+ const deadline = start + (parseInt(process.argv.includes('--timeout') ? process.argv[process.argv.indexOf('--timeout')+1] : '30000'))
57
+ let poll = 0
58
+ let disappeared = false
59
+ const platformArg = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
60
+ const deviceArg = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
61
+ while (Date.now() <= deadline) {
62
+ poll++
63
+ try {
64
+ const f = await ToolsInteract.findElementHandler({ query: 'Generating session', exact: false, timeoutMs: 1000, platform: platformArg, deviceId: deviceArg })
65
+ const exists = f && typeof f.found !== 'undefined' ? f.found : false
66
+ console.log(`poll ${poll}: generating exists=${exists}`)
67
+ if (!exists) { disappeared = true; break }
68
+ } catch (e) { console.error('findElement err:', e) }
69
+ await new Promise(r => setTimeout(r, 500))
70
+ }
71
+
72
+ const elapsed = Date.now()-start
73
+ console.log('Result: disappeared=', disappeared, 'polls=', poll, 'elapsedMs=', elapsed)
74
+
75
+ // Platform-specific post-conditions
76
+
77
+
78
+ console.log('\nFinal Result: success=', disappeared)
79
+
80
+ console.log('\nStopping log stream')
81
+ try { const stop = await ToolsObserve.stopLogStreamHandler({ platform: 'ios', sessionId }); console.log('stopLogStream', stop) } catch (e) { console.error('stop failed', e) }
82
+ process.exit(disappeared ? 0 : 2)
83
+ }
84
+
85
+ main().catch(e=>{ console.error(e); process.exit(1) })
@@ -0,0 +1,123 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { ToolsObserve } from '../observe/index.js'
4
+ import { ToolsInteract } from '../interact/index.js'
5
+
6
+ const argv = process.argv.slice(2)
7
+ const opts = { platform: 'android', deviceId: undefined, timeoutMs: 180000, stabilityMs: 1000, pollIntervalMs: 500, saveDir: '/tmp' }
8
+ for (let i = 0; i < argv.length; i++) {
9
+ if (argv[i] === '--platform') opts.platform = argv[++i]
10
+ else if (argv[i] === '--deviceId') opts.deviceId = argv[++i]
11
+ else if (argv[i] === '--timeout') opts.timeoutMs = Number(argv[++i])
12
+ else if (argv[i] === '--stabilityMs') opts.stabilityMs = Number(argv[++i])
13
+ else if (argv[i] === '--poll') opts.pollIntervalMs = Number(argv[++i])
14
+ else if (argv[i] === '--saveDir') opts.saveDir = argv[++i]
15
+ }
16
+
17
+ const platform = opts.platform
18
+ const deviceId = opts.deviceId
19
+ const timeoutMs = opts.timeoutMs
20
+ const stabilityMs = opts.stabilityMs
21
+ const poll = opts.pollIntervalMs
22
+ const saveDir = opts.saveDir
23
+
24
+ const ts = () => Date.now()
25
+
26
+ async function findAndTapGenerate() {
27
+ try {
28
+ const found = await ToolsInteract.findElementHandler({ query: 'Generate Session', exact: false, timeoutMs: 3000, platform, deviceId })
29
+ const foundFlag = found && typeof found.found !== 'undefined' ? found.found : false
30
+ console.log('find Generate Session:', foundFlag)
31
+ if (foundFlag) {
32
+ const el = found.element
33
+ if (el && el.tapCoordinates) {
34
+ console.log('Tapping at', JSON.stringify(el.tapCoordinates))
35
+ await ToolsInteract.tapHandler({ platform, x: el.tapCoordinates.x, y: el.tapCoordinates.y, deviceId })
36
+ return true
37
+ } else {
38
+ // tap center of parent bounds if available
39
+ if (el && el.bounds) {
40
+ const b = el.bounds
41
+ const cx = Math.floor((b.left + b.right)/2)
42
+ const cy = Math.floor((b.top + b.bottom)/2)
43
+ console.log('Tapping parent center', cx, cy)
44
+ await ToolsInteract.tapHandler({ platform, x: cx, y: cy, deviceId })
45
+ return true
46
+ }
47
+ }
48
+ }
49
+ } catch (e) { console.error('error finding/tapping generate:', e) }
50
+ return false
51
+ }
52
+
53
+ async function captureUITree(savePrefix) {
54
+ try {
55
+ const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId })
56
+ const p = path.join(saveDir, `${savePrefix}_ui_${Date.now()}.json`)
57
+ fs.writeFileSync(p, JSON.stringify(tree, null, 2))
58
+ console.log('Wrote UI tree', p)
59
+ return tree
60
+ } catch (e) { console.error('failed getUITree:', e); return null }
61
+ }
62
+
63
+ async function tryCaptureScreenshot(savePrefix) {
64
+ try {
65
+ const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
66
+ // handler may return { image: 'data:image/png;base64,...' } or blocks; try to extract base64
67
+ let b64 = null
68
+ if (res && res.image && typeof res.image === 'string') b64 = res.image.replace(/^data:image\/png;base64,/, '')
69
+ else if (Array.isArray(res) && res.length) {
70
+ for (const blk of res) { if (blk && blk.mime === 'image/png' && blk.data) { b64 = blk.data; break } }
71
+ } else if (res && res.blocks) {
72
+ for (const blk of res.blocks) { if (blk && blk.mime === 'image/png' && blk.data) { b64 = blk.data; break } }
73
+ }
74
+ if (b64) {
75
+ const p = path.join(saveDir, `${savePrefix}_shot_${Date.now()}.png`)
76
+ fs.writeFileSync(p, Buffer.from(b64, 'base64'))
77
+ console.log('Wrote screenshot', p)
78
+ return p
79
+ }
80
+ console.log('No image returned from captureScreenshotHandler')
81
+ } catch (e) { console.error('screenshot error:', e) }
82
+ return null
83
+ }
84
+
85
+ async function main() {
86
+ console.log('Starting test: tap Generate Session and wait for Play session to appear stably')
87
+ const tapped = await findAndTapGenerate()
88
+ if (!tapped) console.log('Warning: generate tap not performed')
89
+
90
+ const start = ts()
91
+ const deadline = start + timeoutMs
92
+ let matchedAt = null
93
+ let pollCount = 0
94
+
95
+ while (Date.now() <= deadline) {
96
+ pollCount++
97
+ const savePrefix = `gen_wait_p${pollCount}`
98
+ const tree = await captureUITree(savePrefix)
99
+ try {
100
+ const f = await ToolsInteract.findElementHandler({ query: 'Play session', exact: false, timeoutMs: 500, platform, deviceId })
101
+ const exists = f && typeof f.found !== 'undefined' ? f.found : false
102
+ console.log(`poll ${pollCount}: play exists=${exists}`)
103
+ await tryCaptureScreenshot(savePrefix)
104
+ if (exists) {
105
+ if (matchedAt === null) matchedAt = Date.now()
106
+ const stable = Date.now() - matchedAt
107
+ if (stable >= stabilityMs) {
108
+ console.log('SUCCESS: Play session present and stable for', stable, 'ms after', Date.now()-start, 'ms and', pollCount, 'polls')
109
+ process.exit(0)
110
+ }
111
+ } else {
112
+ matchedAt = null
113
+ }
114
+ } catch (e) { console.error('error during poll:', e) }
115
+
116
+ await new Promise(r => setTimeout(r, poll))
117
+ }
118
+
119
+ console.error('FAIL: timeout waiting for Play session (polls=', pollCount, ')')
120
+ process.exit(2)
121
+ }
122
+
123
+ main().catch(e=>{ console.error('fatal', e); process.exit(1) })
package/dist/server.js CHANGED
@@ -315,31 +315,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
315
315
  }
316
316
  },
317
317
  {
318
- name: "wait_for_element",
319
- description: "Wait until a UI element with matching text appears on screen or timeout is reached.",
318
+ name: "wait_for_ui",
319
+ description: "Wait for a UI/log/screen/idle condition with a stability window before returning success.",
320
320
  inputSchema: {
321
321
  type: "object",
322
322
  properties: {
323
- platform: {
324
- type: "string",
325
- enum: ["android", "ios"],
326
- description: "Platform to check"
327
- },
328
- text: {
329
- type: "string",
330
- description: "Text content of the element to wait for"
331
- },
332
- timeout: {
333
- type: "number",
334
- description: "Max wait time in ms (default 10000)",
335
- default: 10000
336
- },
337
- deviceId: {
338
- type: "string",
339
- description: "Device Serial/UDID. Defaults to connected/booted device."
340
- }
341
- },
342
- required: ["platform", "text"]
323
+ type: { type: "string", enum: ["ui", "log", "screen", "idle"], description: "Condition type to observe", default: "ui" },
324
+ query: { type: "string", description: "Optional query string for ui/log/screen types" },
325
+ timeoutMs: { type: "number", description: "Timeout in ms to wait for condition (default 30000)", default: 30000 },
326
+ pollIntervalMs: { type: "number", description: "Polling interval in ms (default 300, clamped to 250-500)", default: 300 },
327
+ match: { type: "string", enum: ["present", "absent"], description: "Match mode for UI checks: 'present' or 'absent' (default 'present')", default: "present" },
328
+ stability_ms: { type: "number", description: "Stability window in ms that the condition must hold before returning success (default 700)", default: 700 },
329
+ includeSnapshotOnFailure: { type: "boolean", description: "Whether to include a debug snapshot on timeout (default true)", default: true },
330
+ platform: { type: "string", enum: ["android", "ios"], description: "Optional platform override" },
331
+ deviceId: { type: "string", description: "Optional device serial/udid" }
332
+ }
343
333
  }
344
334
  },
345
335
  {
@@ -626,9 +616,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
626
616
  const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId });
627
617
  return wrapResponse(res);
628
618
  }
629
- if (name === "wait_for_element") {
630
- const { platform, text, timeout, deviceId } = (args || {});
631
- const res = await ToolsInteract.waitForElementHandler({ platform, text, timeout, deviceId });
619
+ if (name === "wait_for_ui") {
620
+ const { type = 'ui', query, timeoutMs = 30000, pollIntervalMs = 300, includeSnapshotOnFailure = true, match = 'present', stability_ms = 700, observationDelayMs = 0, platform, deviceId } = (args || {});
621
+ const res = await ToolsInteract.waitForUIHandler({ type, query, timeoutMs, pollIntervalMs, includeSnapshotOnFailure, match, stability_ms, observationDelayMs, platform, deviceId });
632
622
  return wrapResponse(res);
633
623
  }
634
624
  if (name === "find_element") {
@@ -19,7 +19,7 @@ function readPropertiesFile(p) {
19
19
  }
20
20
  return out;
21
21
  }
22
- catch (e) {
22
+ catch {
23
23
  return {};
24
24
  }
25
25
  }
@@ -35,7 +35,7 @@ function javaBinExists(p) {
35
35
  return true;
36
36
  return false;
37
37
  }
38
- catch (e) {
38
+ catch {
39
39
  return false;
40
40
  }
41
41
  }
@@ -67,7 +67,7 @@ export async function checkGradle() {
67
67
  }
68
68
  }
69
69
  }
70
- catch (e) { /* ignore */ }
70
+ catch { }
71
71
  // 3) system gradle.properties
72
72
  const systemProps = '/etc/gradle/gradle.properties';
73
73
  filesChecked.push(systemProps);
@@ -82,7 +82,7 @@ export async function checkGradle() {
82
82
  }
83
83
  }
84
84
  }
85
- catch (e) { /* ignore */ }
85
+ catch { }
86
86
  // 4) GRADLE_HOME fallback
87
87
  if (!gradleJavaHome && process.env.GRADLE_HOME) {
88
88
  filesChecked.push(process.env.GRADLE_HOME);
@@ -82,7 +82,7 @@ export async function prepareGradle(projectPath) {
82
82
  try {
83
83
  gradleCheck = await checkGradle();
84
84
  }
85
- catch (e) {
85
+ catch {
86
86
  gradleCheck = { gradleJavaHome: undefined, gradleValid: false, filesChecked: [], issues: [] };
87
87
  }
88
88
  const env = Object.assign({}, process.env);
@@ -127,7 +127,7 @@ export async function prepareGradle(projectPath) {
127
127
  try {
128
128
  delete env.GRADLE_JAVA_HOME;
129
129
  }
130
- catch (e) { }
130
+ catch { }
131
131
  }
132
132
  }
133
133
  if (platformToolsDir) {
@@ -23,6 +23,11 @@ export async function listDevices(platform, appId) {
23
23
  export async function resolveTargetDevice(opts) {
24
24
  const { platform, appId, prefer, deviceId } = opts;
25
25
  const devices = await listDevices(platform, appId);
26
+ // During unit tests (no adb/xcrun available), provide a lightweight mock device so
27
+ // the observe/interact unit tests can run without real devices.
28
+ if ((!devices || devices.length === 0) && (process.env.NODE_ENV === 'test' || process.env.MCP_TEST_MOCK_DEVICES === '1')) {
29
+ return { id: 'mock', platform: platform || 'android', osVersion: '12', model: 'Pixel', simulator: true };
30
+ }
26
31
  if (deviceId) {
27
32
  const found = devices.find(d => d.id === deviceId);
28
33
  if (!found)
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## [0.21.1]
6
+ - Removed wait_for_element and renamed observe_until to wait_for_ui (obsolete references removed)
7
+
8
+ ## [0.21.0]
9
+ - Added `wait_for_ui` as a tool for agents to wait for things like API requests
10
+
5
11
  ## [0.20.1]
6
12
  - Fixes gradle home issue for android
7
13
 
@@ -21,7 +27,7 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
21
27
 
22
28
  ## [0.19.0]
23
29
 
24
- - Added `observe_until` interaction tool: waits for UI, log, screen fingerprint or idle conditions with configurable polling and timeout. Returns rich details on match (element info, log line, new fingerprint).
30
+ - Added `wait_for_ui` interaction tool: waits for UI, log, screen fingerprint or idle conditions with configurable polling and timeout. Returns rich details on match (element info, log line, new fingerprint).
25
31
 
26
32
 
27
33
  ## [0.18.0]
@@ -2,26 +2,6 @@
2
2
 
3
3
  Tools that perform UI interactions: tap, swipe, type_text, press_back, and waiting for elements.
4
4
 
5
- ## wait_for_element
6
- Wait until a UI element with matching text appears on screen or timeout is reached.
7
-
8
- Input:
9
-
10
- ```
11
- { "platform": "android", "text": "Home", "timeout": 5000, "deviceId": "emulator-5554" }
12
- ```
13
-
14
- Response:
15
-
16
- ```
17
- { "device": { "platform": "android", "id": "emulator-5554" }, "found": true, "element": { "text": "Home", "resourceId": "com.example:id/home" } }
18
- ```
19
-
20
- Notes:
21
- - Polls get_ui_tree until timeout or element found. Returns an `error` field if system failures occur.
22
-
23
- ---
24
-
25
5
  ## tap / swipe / type_text / press_back
26
6
 
27
7
  Tap input example:
@@ -153,7 +133,7 @@ Notes:
153
133
 
154
134
  ---
155
135
 
156
- ## observe_until
136
+ ## wait_for_ui
157
137
 
158
138
  Purpose:
159
139
  - Wait for a condition to occur on the device: UI element appearance, a log line, a screen fingerprint change, or an idle/stable screen state.
@@ -164,7 +144,7 @@ Supported types and behavior:
164
144
  - screen: Compares screen fingerprints (visual checks) against an initial baseline and returns when fingerprint changes. If `query` is provided it will attempt a `find_element` on the new screen to validate the expected content.
165
145
  - idle: Waits until the screen fingerprint remains stable for a short stability window (default 1000ms).
166
146
 
167
- Input (ToolsInteract.observeUntilHandler):
147
+ Input (ToolsInteract.waitForUIHandler):
168
148
  ```
169
149
  { "type": "ui|log|screen|idle", "query": "optional string", "timeoutMs": 5000, "pollIntervalMs": 200, "platform": "android|ios", "deviceId": "optional device id" }
170
150
  ```
@@ -190,16 +170,16 @@ Notes & tips:
190
170
  - For UI-sensitive flows prefer type='ui' rather than relying solely on visual fingerprint changes, as some UI updates don't alter the fingerprint.
191
171
 
192
172
  Tests:
193
- - Unit: `test/interact/unit/observe_until.test.ts`
194
- - Device runner: `test/interact/device/observe_until_device.ts` (requires devices/emulators and adb/xcrun in PATH)
173
+ - Unit: `test/interact/unit/wait_for_ui.test.ts`
174
+ - Device runner: `test/interact/device/wait_for_ui_device.ts` (requires devices/emulators and adb/xcrun in PATH)
195
175
 
196
176
  Example:
197
177
  ```
198
178
  // Wait up to 5s for a button labeled "Generate Session" on Android
199
- ToolsInteract.observeUntilHandler({ type: 'ui', query: 'Generate Session', timeoutMs: 5000, platform: 'android' })
179
+ ToolsInteract.waitForUIHandler({ type: 'ui', query: 'Generate Session', timeoutMs: 5000, platform: 'android' })
200
180
  ```
201
181
 
202
182
  Troubleshooting:
203
- - If observe_until(log) never matches, ensure log streaming is started for the target package and baseline logs captured correctly.
204
- - If observe_until(screen) times out despite visible UI change, try type='ui' to validate content-level changes.
183
+ - If wait_for_ui(log) never matches, ensure log streaming is started for the target package and baseline logs captured correctly.
184
+ - If wait_for_ui(screen) times out despite visible UI change, try type='ui' to validate content-level changes.
205
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.20.1",
3
+ "version": "0.21.1",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "healthcheck": "tsx ./src/cli/idb/check-idb.ts",
14
14
  "install-idb": "tsx ./src/cli/idb/install-idb.ts",
15
15
  "preflight-ios": "tsx ./src/cli/ios/preflight-ios.ts",
16
- "test:unit": "tsx test/unit/index.ts",
16
+ "test:unit": "SKIP_DEVICE_TESTS=1 tsx test/unit/index.ts",
17
17
  "test:integration": "npm run build && tsx test/device/index.ts",
18
18
  "test:device": "npm run build && tsx test/device/index.ts",
19
19
  "test": "npm run test:unit",
@@ -1,4 +1,4 @@
1
- import { WaitForElementResponse, TapResponse, SwipeResponse, TypeTextResponse, PressBackResponse } from "../types.js"
1
+ import { TapResponse, SwipeResponse, TypeTextResponse, PressBackResponse } from "../types.js"
2
2
  import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "../utils/android/utils.js"
3
3
  import { AndroidObserve } from "../observe/index.js"
4
4
  import { scrollToElementShared } from "../utils/ui/index.js"
@@ -7,37 +7,6 @@ import { scrollToElementShared } from "../utils/ui/index.js"
7
7
  export class AndroidInteract {
8
8
  private observe = new AndroidObserve();
9
9
 
10
- async waitForElement(text: string, timeout: number, deviceId?: string): Promise<WaitForElementResponse> {
11
- const metadata = await getAndroidDeviceMetadata("", deviceId)
12
- const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
13
- const startTime = Date.now();
14
-
15
- while (Date.now() - startTime < timeout) {
16
- try {
17
- const tree = await this.observe.getUITree(deviceId);
18
-
19
- if (tree.error) {
20
- return { device: deviceInfo, found: false, error: tree.error };
21
- }
22
-
23
- const element = tree.elements.find(e => e.text === text);
24
- if (element) {
25
- return { device: deviceInfo, found: true, element };
26
- }
27
- } catch (e) {
28
- // Ignore errors during polling and retry
29
- console.error("Error polling UI tree:", e);
30
- }
31
-
32
- const elapsed = Date.now() - startTime;
33
- const remaining = timeout - elapsed;
34
- if (remaining <= 0) break;
35
-
36
- await new Promise(resolve => setTimeout(resolve, Math.min(500, remaining)));
37
- }
38
- return { device: deviceInfo, found: false };
39
- }
40
-
41
10
  async tap(x: number, y: number, deviceId?: string): Promise<TapResponse> {
42
11
  const metadata = await getAndroidDeviceMetadata("", deviceId)
43
12
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)