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/dist/interact/index.js +120 -15
- package/dist/manage/android.js +24 -4
- package/dist/manage/ios.js +27 -3
- package/dist/server/common.js +4 -2
- package/dist/server/tool-handlers.js +19 -2
- package/dist/system/index.js +53 -2
- package/docs/CHANGELOG.md +5 -0
- package/docs/tools/interact.md +73 -7
- package/docs/tools/manage.md +16 -1
- package/docs/tools/system.md +23 -1
- package/package.json +1 -1
- package/src/interact/index.ts +121 -15
- package/src/manage/android.ts +24 -4
- package/src/manage/ios.ts +27 -3
- package/src/server/common.ts +8 -2
- package/src/server/tool-handlers.ts +19 -2
- package/src/system/index.ts +57 -2
- package/src/types.ts +36 -0
- package/test/unit/interact/expect_tools.test.ts +29 -2
- package/test/unit/interact/wait_for_screen_change.test.ts +7 -3
- package/test/unit/interact/wait_for_ui_selector_matching.test.ts +19 -0
- package/test/unit/server/response_shapes.test.ts +20 -3
- package/test/unit/system/get_system_status.test.ts +2 -0
- package/test/unit/system/system_status.test.ts +6 -0
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
|
-
|
|
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 {
|