mobile-debug-mcp 0.24.1 → 0.24.2

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/src/types.ts CHANGED
@@ -10,6 +10,15 @@ export interface StartAppResponse {
10
10
  device: DeviceInfo;
11
11
  appStarted: boolean;
12
12
  launchTimeMs: number;
13
+ output?: string;
14
+ observedApp?: {
15
+ appId: string;
16
+ package?: string | null;
17
+ activity?: string | null;
18
+ screen?: string | null;
19
+ pid?: number | null;
20
+ matchedTarget?: boolean | null;
21
+ };
13
22
  error?: string;
14
23
  diagnostics?: any;
15
24
  }
@@ -25,6 +34,17 @@ export interface RestartAppResponse {
25
34
  device: DeviceInfo;
26
35
  appRestarted: boolean;
27
36
  launchTimeMs: number;
37
+ output?: string;
38
+ observedApp?: {
39
+ appId: string;
40
+ package?: string | null;
41
+ activity?: string | null;
42
+ screen?: string | null;
43
+ pid?: number | null;
44
+ matchedTarget?: boolean | null;
45
+ };
46
+ terminatedBeforeRestart?: boolean;
47
+ terminateError?: string;
28
48
  error?: string;
29
49
  diagnostics?: any;
30
50
  }
@@ -155,6 +175,7 @@ export interface ActionExecutionResult {
155
175
  action_id: string;
156
176
  timestamp: number;
157
177
  action_type: string;
178
+ device?: DeviceInfo;
158
179
  target: {
159
180
  selector: Record<string, unknown> | null;
160
181
  resolved: ActionTargetResolved | null;
@@ -164,6 +185,7 @@ export interface ActionExecutionResult {
164
185
  retryable?: boolean;
165
186
  ui_fingerprint_before?: string | null;
166
187
  ui_fingerprint_after?: string | null;
188
+ details?: Record<string, unknown>;
167
189
  }
168
190
 
169
191
  export interface TapElementResponse extends ActionExecutionResult {}
@@ -179,6 +201,11 @@ export interface ExpectScreenResponse {
179
201
  screen: string | null;
180
202
  };
181
203
  confidence: number;
204
+ comparison: {
205
+ basis: 'fingerprint' | 'screen' | 'none';
206
+ matched: boolean;
207
+ reason: string;
208
+ };
182
209
  }
183
210
 
184
211
  export interface ExpectElementVisibleResponse {
@@ -190,7 +217,16 @@ export interface ExpectElementVisibleResponse {
190
217
  contains?: boolean;
191
218
  };
192
219
  element_id: string | null;
220
+ expected_condition?: 'visible';
193
221
  element?: ActionTargetResolved | null;
222
+ observed?: {
223
+ status?: string;
224
+ matched_count?: number;
225
+ condition_satisfied?: boolean;
226
+ selected_index?: number | null;
227
+ last_matched_element?: ActionTargetResolved | null;
228
+ };
229
+ reason?: string;
194
230
  failure_code?: 'TIMEOUT' | 'ELEMENT_NOT_FOUND' | 'UNKNOWN';
195
231
  retryable?: boolean;
196
232
  }
@@ -15,7 +15,12 @@ async function run() {
15
15
  success: true,
16
16
  observed_screen: { fingerprint: 'fp_home', screen: 'com.example.HomeActivity' },
17
17
  expected_screen: { fingerprint: 'fp_home', screen: null },
18
- confidence: 1
18
+ confidence: 1,
19
+ comparison: {
20
+ basis: 'fingerprint',
21
+ matched: true,
22
+ reason: 'observed fingerprint matches expected fingerprint fp_home'
23
+ }
19
24
  })
20
25
 
21
26
  ;(Observe as any).ToolsObserve.getCurrentScreenHandler = async () => ({
@@ -26,6 +31,11 @@ async function run() {
26
31
  assert.strictEqual(expectScreen.success, true)
27
32
  assert.strictEqual(expectScreen.observed_screen.screen, 'HomeActivity')
28
33
  assert.strictEqual(expectScreen.confidence, 1)
34
+ assert.deepStrictEqual(expectScreen.comparison, {
35
+ basis: 'screen',
36
+ matched: true,
37
+ reason: 'observed screen matches expected screen HomeActivity'
38
+ })
29
39
 
30
40
  ;(ToolsInteract as any).waitForUIHandler = async () => ({
31
41
  status: 'success',
@@ -46,10 +56,18 @@ async function run() {
46
56
  assert.strictEqual(expectElementVisible.success, true)
47
57
  assert.strictEqual(expectElementVisible.element_id, 'el_ready')
48
58
  assert.strictEqual(expectElementVisible.element?.resource_id, 'rid_ready')
59
+ assert.strictEqual(expectElementVisible.expected_condition, 'visible')
60
+ assert.strictEqual(expectElementVisible.reason, 'selector is visible')
49
61
 
50
62
  ;(ToolsInteract as any).waitForUIHandler = async () => ({
51
63
  status: 'timeout',
52
- error: { code: 'ELEMENT_NOT_FOUND', message: 'Condition visible not satisfied within timeout' }
64
+ error: { code: 'ELEMENT_NOT_FOUND', message: 'Condition visible not satisfied within timeout; observed 0 match(es)' },
65
+ observed: {
66
+ matched_count: 0,
67
+ condition_satisfied: false,
68
+ selected_index: null,
69
+ last_matched_element: null
70
+ }
53
71
  })
54
72
  const timeoutResult = await ToolsInteract.expectElementVisibleHandler({
55
73
  selector: { text: 'Missing' },
@@ -59,6 +77,15 @@ async function run() {
59
77
  success: false,
60
78
  selector: { text: 'Missing' },
61
79
  element_id: null,
80
+ expected_condition: 'visible',
81
+ observed: {
82
+ status: 'timeout',
83
+ matched_count: 0,
84
+ condition_satisfied: false,
85
+ selected_index: null,
86
+ last_matched_element: null
87
+ },
88
+ reason: 'Condition visible not satisfied within timeout; observed 0 match(es)',
62
89
  failure_code: 'TIMEOUT',
63
90
  retryable: true
64
91
  })
@@ -9,22 +9,26 @@ async function runTests() {
9
9
 
10
10
  try {
11
11
  let seq1: Array<string | null> = ['B', 'B']
12
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq1.length ? seq1.shift() : null })
12
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq1.length ? seq1.shift() : null, activity: 'MainActivity' })
13
13
  const start1 = Date.now()
14
14
  const res1 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 2000, pollIntervalMs: 50 })
15
15
  const elapsed1 = Date.now() - start1
16
16
  assert.ok(res1 && (res1 as any).success === true && (res1 as any).newFingerprint === 'B', 'Immediate fingerprint change should succeed')
17
+ assert.strictEqual((res1 as any).previousFingerprint, 'A')
18
+ assert.strictEqual((res1 as any).observed_screen.activity, 'MainActivity')
17
19
  console.log('Test 1: Immediate change -> PASS', 'Elapsed:', elapsed1, 'ms')
18
20
 
19
21
  let seq2: Array<string | null> = [null, null, 'B', 'B']
20
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq2.length ? seq2.shift() : 'B' })
22
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq2.length ? seq2.shift() : 'B', activity: 'NextActivity' })
21
23
  const res2 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 3000, pollIntervalMs: 50 })
22
24
  assert.ok(res2 && (res2 as any).success === true && (res2 as any).newFingerprint === 'B', 'Transient nulls should not prevent success')
23
25
  console.log('Test 2: Transient nulls -> PASS')
24
26
 
25
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'A' })
27
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'A', activity: 'HomeActivity' })
26
28
  const res3 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 300, pollIntervalMs: 50 })
27
29
  assert.ok(res3 && (res3 as any).success === false && (res3 as any).reason === 'timeout', 'Unchanged fingerprint should time out')
30
+ assert.strictEqual((res3 as any).previousFingerprint, 'A')
31
+ assert.strictEqual((res3 as any).observed_screen.activity, 'HomeActivity')
28
32
  console.log('Test 3: Timeout -> PASS')
29
33
  } finally {
30
34
  ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = original
@@ -12,6 +12,12 @@ async function run() {
12
12
  const r1 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hello' }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
13
13
  const ok1 = r1 && r1.status === 'success' && r1.matched === 1 && r1.element && r1.element.text === 'Hello' && typeof r1.element.elementId === 'string'
14
14
  assert.ok(ok1, 'Exact match should satisfy exists condition')
15
+ assert.deepStrictEqual((r1 as any).requested, {
16
+ selector: { text: 'Hello' },
17
+ condition: 'exists',
18
+ match: null
19
+ })
20
+ assert.strictEqual((r1 as any).observed.matched_count, 1)
15
21
  console.log('Exact match exists:', ok1 ? 'PASS' : 'FAIL', JSON.stringify(r1, null, 2))
16
22
 
17
23
  // Test 2: contains matching
@@ -26,6 +32,9 @@ async function run() {
26
32
  const r3 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hidden' }, condition: 'visible', timeout_ms: 300, poll_interval_ms: 50, platform: 'android' })
27
33
  const ok3 = r3 && r3.status === 'timeout' && r3.error && r3.error.code === 'ELEMENT_NOT_FOUND'
28
34
  assert.ok(ok3, 'Hidden element should fail visible condition')
35
+ assert.strictEqual((r3 as any).observed.matched_count, 1)
36
+ assert.strictEqual((r3 as any).observed.condition_satisfied, false)
37
+ assert.match((r3 as any).error.message, /observed 1 match/)
29
38
  console.log('Visible negative (hidden element):', ok3 ? 'PASS' : 'FAIL', JSON.stringify(r3, null, 2))
30
39
 
31
40
  // Test 4: clickable condition
@@ -66,8 +75,18 @@ async function run() {
66
75
  const r7 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Nope' }, condition: 'exists', timeout_ms: 300, poll_interval_ms: 50, retry: { max_attempts: 1 }, platform: 'android' })
67
76
  const ok7 = r7 && r7.status === 'timeout' && r7.error && r7.error.code === 'ELEMENT_NOT_FOUND'
68
77
  assert.ok(ok7, 'Missing selector should time out with ELEMENT_NOT_FOUND')
78
+ assert.strictEqual((r7 as any).observed.selected_index, null)
69
79
  console.log('Timeout no match:', ok7 ? 'PASS' : 'FAIL', JSON.stringify(r7, null, 2))
70
80
 
81
+ // Test 8: requested match index out of range should not report a selected index
82
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'OnlyOne', resourceId: 'rid8', bounds: [0,0,10,10], visible: true } ] })
83
+ const r8 = await ToolsInteract.waitForUIHandler({ selector: { text: 'OnlyOne' }, condition: 'exists', timeout_ms: 300, poll_interval_ms: 50, match: { index: 1 }, platform: 'android' })
84
+ const ok8 = r8 && r8.status === 'timeout' && r8.error && r8.error.code === 'ELEMENT_NOT_FOUND'
85
+ assert.ok(ok8, 'Out-of-range match index should time out deterministically')
86
+ assert.strictEqual((r8 as any).observed.matched_count, 1)
87
+ assert.strictEqual((r8 as any).observed.selected_index, null)
88
+ console.log('Out-of-range match index:', ok8 ? 'PASS' : 'FAIL', JSON.stringify(r8, null, 2))
89
+
71
90
  } finally {
72
91
  ;(Observe as any).ToolsObserve.getUITreeHandler = origGetUITree
73
92
  }
@@ -78,38 +78,55 @@ async function run() {
78
78
  return {
79
79
  device: { platform: 'android', id: 'emulator-5554', osVersion: '14', model: 'Pixel', simulator: true },
80
80
  appStarted: true,
81
- launchTimeMs: 123
81
+ launchTimeMs: 123,
82
+ output: 'Events injected: 1',
83
+ observedApp: {
84
+ appId: 'com.example.app',
85
+ package: 'com.example.app',
86
+ activity: 'com.example.MainActivity',
87
+ screen: 'MainActivity',
88
+ matchedTarget: true
89
+ }
82
90
  } as any
83
91
  }
84
92
  const startAppResponse = await handleToolCall('start_app', { platform: 'android', appId: 'com.example.app' })
85
93
  const startAppPayload = JSON.parse((startAppResponse as any).content[0].text)
86
94
  assert.strictEqual(startAppPayload.success, true)
87
95
  assert.strictEqual(startAppPayload.action_type, 'start_app')
96
+ assert.strictEqual(startAppPayload.device.id, 'emulator-5554')
88
97
  assert.deepStrictEqual(startAppPayload.target.selector, { appId: 'com.example.app' })
98
+ assert.strictEqual(startAppPayload.details.launch_time_ms, 123)
99
+ assert.strictEqual(startAppPayload.details.observed_app.matchedTarget, true)
89
100
 
90
101
  ;(ToolsInteract as any).expectScreenHandler = async () => ({
91
102
  success: true,
92
103
  observed_screen: { fingerprint: 'fp_after', screen: 'MainActivity' },
93
104
  expected_screen: { fingerprint: 'fp_after', screen: null },
94
- confidence: 1
105
+ confidence: 1,
106
+ comparison: { basis: 'fingerprint', matched: true, reason: 'observed fingerprint matches expected fingerprint fp_after' }
95
107
  })
96
108
 
97
109
  const expectScreenResponse = await handleToolCall('expect_screen', { fingerprint: 'fp_after' })
98
110
  const expectScreenPayload = JSON.parse((expectScreenResponse as any).content[0].text)
99
111
  assert.strictEqual(expectScreenPayload.success, true)
100
112
  assert.strictEqual(expectScreenPayload.confidence, 1)
113
+ assert.strictEqual(expectScreenPayload.comparison.basis, 'fingerprint')
101
114
 
102
115
  ;(ToolsInteract as any).expectElementVisibleHandler = async () => ({
103
116
  success: true,
104
117
  selector: { text: 'Ready' },
105
118
  element_id: 'el_ready',
106
- element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0 }
119
+ expected_condition: 'visible',
120
+ element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0 },
121
+ observed: { status: 'success', matched_count: 1, condition_satisfied: true, selected_index: 0, last_matched_element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0 } },
122
+ reason: 'selector is visible'
107
123
  })
108
124
 
109
125
  const expectElementResponse = await handleToolCall('expect_element_visible', { selector: { text: 'Ready' } })
110
126
  const expectElementPayload = JSON.parse((expectElementResponse as any).content[0].text)
111
127
  assert.strictEqual(expectElementPayload.success, true)
112
128
  assert.strictEqual(expectElementPayload.element_id, 'el_ready')
129
+ assert.strictEqual(expectElementPayload.expected_condition, 'visible')
113
130
 
114
131
  ;(ToolsObserve as any).captureScreenshotHandler = async () => ({
115
132
  device: { platform: 'ios', id: 'booted', osVersion: '18.0', model: 'Simulator', simulator: true },
@@ -6,7 +6,9 @@ import { getSystemStatus } from '../../../src/system/index.js'
6
6
  async function run() {
7
7
  const payload = await getSystemStatus()
8
8
  assert(typeof payload.success === 'boolean')
9
+ assert(typeof (payload as any).status === 'string')
9
10
  assert(Array.isArray(payload.issues))
11
+ assert((payload as any).summary && typeof (payload as any).summary.overall === 'string')
10
12
 
11
13
  const adb = ensureAdbAvailable()
12
14
  assert(adb && typeof adb.ok === 'boolean')
@@ -93,8 +93,11 @@ async function run() {
93
93
 
94
94
  const healthy = await systemStatus.getSystemStatus()
95
95
  assert.strictEqual(healthy.success, true)
96
+ assert.strictEqual((healthy as any).status, 'ready')
96
97
  assert.strictEqual(healthy.adbAvailable, true)
97
98
  assert.strictEqual(typeof healthy.adbVersion, 'string')
99
+ assert.strictEqual((healthy as any).summary.android.ready, true)
100
+ assert.strictEqual(typeof (healthy as any).summary.ios.summary, 'string')
98
101
 
99
102
  setScenario({
100
103
  ADB_VERSION_STATUS: '1',
@@ -105,6 +108,7 @@ async function run() {
105
108
  })
106
109
  const missingAdb = await systemStatus.getSystemStatus()
107
110
  assert.strictEqual(missingAdb.success, false)
111
+ assert.strictEqual((missingAdb as any).summary.android.ready, false)
108
112
  assert(missingAdb.issues.some((issue: string) => issue.includes('ADB')))
109
113
 
110
114
  setScenario({
@@ -116,6 +120,7 @@ async function run() {
116
120
  })
117
121
  const unauthorized = await systemStatus.getSystemStatus()
118
122
  assert.strictEqual(unauthorized.success, false)
123
+ assert.strictEqual((unauthorized as any).summary.android.ready, false)
119
124
  assert(unauthorized.issues.some((issue: string) => issue.includes('unauthorized')))
120
125
  assert(unauthorized.issues.some((issue: string) => issue.includes('offline')))
121
126
 
@@ -130,6 +135,7 @@ async function run() {
130
135
  assert.strictEqual(missingXcrun.iosAvailable, false)
131
136
  assert.strictEqual(missingXcrun.adbAvailable, true)
132
137
  assert.strictEqual(Array.isArray(missingXcrun.issues), true)
138
+ assert.strictEqual((missingXcrun as any).summary.ios.ready, false)
133
139
 
134
140
  console.log('system_status checks passed')
135
141
  } finally {