mobile-debug-mcp 0.30.2 → 0.32.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.
- package/README.md +50 -5
- package/dist/interact/android.js +13 -8
- package/dist/interact/index.js +33 -22
- package/dist/interact/ios.js +5 -2
- package/dist/manage/android.js +20 -10
- package/dist/manage/ios.js +20 -11
- package/dist/observe/android.js +6 -1
- package/dist/observe/index.js +22 -5
- package/dist/observe/ios.js +7 -2
- package/dist/observe/snapshot-metadata.js +94 -15
- package/dist/server/common.js +14 -7
- package/dist/server/journey-schema.js +60 -0
- package/dist/server/tool-definitions.js +59 -6
- package/dist/server/tool-handlers.js +358 -55
- package/dist/server-core.js +1 -1
- package/dist/system/index.js +485 -48
- package/dist/system/ios.js +15 -7
- package/dist/utils/android/utils.js +4 -4
- package/dist/utils/cli/idb/check-idb.js +10 -3
- package/dist/utils/cli/idb/idb-helper.js +47 -13
- package/dist/utils/exec.js +4 -2
- package/dist/utils/ios/utils.js +10 -11
- package/dist/utils/operation-budget.js +25 -0
- package/dist/utils/resolve-device.js +12 -1
- package/dist/utils/timing.js +19 -0
- package/dist/utils/ui/index.js +2 -0
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +4 -4
- package/docs/benchmarks/interaction-latency/README.md +26 -0
- package/docs/benchmarks/interaction-latency/final.json +1810 -0
- package/docs/benchmarks/interaction-latency/final.md +17 -0
- package/docs/benchmarks/interaction-latency/ios-validation.md +11 -0
- package/docs/specs/baseline-spec-v0.md +1 -1
- package/docs/specs-deltas/016-environment-tools.md +533 -0
- package/docs/specs-deltas/018-interaction-latency-and-batched-journeys.md +301 -0
- package/docs/tools/TOOLS.md +2 -0
- package/docs/tools/interact.md +30 -0
- package/docs/tools/manage.md +4 -2
- package/docs/tools/system.md +9 -2
- package/package.json +1 -1
- package/src/interact/android.ts +13 -8
- package/src/interact/index.ts +32 -23
- package/src/interact/ios.ts +5 -2
- package/src/manage/android.ts +19 -10
- package/src/manage/ios.ts +19 -11
- package/src/observe/android.ts +5 -1
- package/src/observe/index.ts +21 -5
- package/src/observe/ios.ts +6 -2
- package/src/observe/snapshot-metadata.ts +94 -15
- package/src/server/common.ts +13 -6
- package/src/server/journey-schema.ts +63 -0
- package/src/server/tool-definitions.ts +60 -7
- package/src/server/tool-handlers.ts +324 -53
- package/src/server-core.ts +1 -1
- package/src/system/index.ts +572 -50
- package/src/system/ios.ts +14 -7
- package/src/types.ts +31 -2
- package/src/utils/android/utils.ts +3 -3
- package/src/utils/cli/idb/check-idb.ts +11 -2
- package/src/utils/cli/idb/idb-helper.ts +49 -12
- package/src/utils/exec.ts +4 -2
- package/src/utils/ios/utils.ts +9 -10
- package/src/utils/operation-budget.ts +22 -0
- package/src/utils/resolve-device.ts +13 -1
- package/src/utils/timing.ts +15 -0
- package/src/utils/ui/index.ts +2 -0
- package/test/device/automated/latency/benchmark.ts +136 -0
- package/test/device/automated/latency/report.ts +6 -0
- package/test/fixtures/latency-journey-app/AndroidManifest.xml +6 -0
- package/test/fixtures/latency-journey-app/README.md +10 -0
- package/test/fixtures/latency-journey-app/build.ts +26 -0
- package/test/fixtures/latency-journey-app/src/MainActivity.java +48 -0
- package/test/unit/interact/tap_element.test.ts +1 -1
- package/test/unit/manage/restart_budget.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +6 -0
- package/test/unit/observe/snapshot_metadata.test.ts +20 -1
- package/test/unit/observe/state_delta.test.ts +15 -0
- package/test/unit/server/benchmark_report.test.ts +4 -0
- package/test/unit/server/contract.test.ts +1 -1
- package/test/unit/server/interaction_modes.test.ts +60 -0
- package/test/unit/server/operation_budget.test.ts +32 -0
- package/test/unit/server/response_shapes.test.ts +3 -3
- package/test/unit/server/run_journey.test.ts +90 -0
- package/test/unit/server/tap_selector.test.ts +68 -0
- package/test/unit/system/get_system_status.test.ts +3 -0
- package/test/unit/system/system_status.test.ts +40 -0
- package/test/unit/utils/idb_health.test.ts +74 -0
package/src/server/common.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordTiming } from '../utils/timing.js'
|
|
1
2
|
import type {
|
|
2
3
|
ActionTrace,
|
|
3
4
|
ActionExecutionResult,
|
|
@@ -14,13 +15,17 @@ import { ToolsObserve } from '../observe/index.js'
|
|
|
14
15
|
export const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3
|
|
15
16
|
export const DEFAULT_MAX_RETRY_DEPTH = 3
|
|
16
17
|
|
|
17
|
-
export function wrapResponse<T>(data: T) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
export function wrapResponse<T>(data: T, includeSerializationTiming = true) {
|
|
19
|
+
const start = performance.now()
|
|
20
|
+
let text = JSON.stringify(data, null, 2)
|
|
21
|
+
const elapsed = performance.now() - start
|
|
22
|
+
const value = data as any
|
|
23
|
+
if (includeSerializationTiming && value && typeof value === 'object' && value.timing && typeof value.timing === 'object') {
|
|
24
|
+
value.timing.serialization_ms = (value.timing.serialization_ms ?? 0) + elapsed
|
|
25
|
+
recordTiming('serialization_ms', elapsed)
|
|
26
|
+
text = JSON.stringify(value, null, 2)
|
|
23
27
|
}
|
|
28
|
+
return { content: [{ type: 'text' as const, text }] }
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export type ToolCallArgs = Record<string, unknown>
|
|
@@ -312,6 +317,7 @@ function mapFailureCodeToFailureClass(code: ActionFailureCode): FailureClass {
|
|
|
312
317
|
case 'STALE_REFERENCE':
|
|
313
318
|
return 'TargetResolutionFailure'
|
|
314
319
|
case 'ELEMENT_NOT_INTERACTABLE':
|
|
320
|
+
case 'ACTION_TIMEOUT':
|
|
315
321
|
return 'ExecutionFailure'
|
|
316
322
|
case 'TIMEOUT':
|
|
317
323
|
case 'ACTION_REJECTED':
|
|
@@ -320,6 +326,7 @@ function mapFailureCodeToFailureClass(code: ActionFailureCode): FailureClass {
|
|
|
320
326
|
return 'ExecutionFailure'
|
|
321
327
|
case 'VERIFICATION_FAILED':
|
|
322
328
|
case 'EXPECT_STATE_MISMATCH':
|
|
329
|
+
case 'ASSERTION_TIMEOUT':
|
|
323
330
|
return 'VerificationFailure'
|
|
324
331
|
case 'CONTROL_CONVERGENCE_FAILED':
|
|
325
332
|
return 'ControlConvergenceFailure'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const selector = z.object({
|
|
4
|
+
text: z.string().min(1).optional(), resource_id: z.string().min(1).optional(),
|
|
5
|
+
accessibility_id: z.string().min(1).optional(), contains: z.boolean().optional()
|
|
6
|
+
}).strict().refine(value => !!(value.text || value.resource_id || value.accessibility_id), 'Selector requires a target')
|
|
7
|
+
const verificationMode = z.enum(['none', 'light', 'full'])
|
|
8
|
+
const timeoutMs = z.number().int().min(100).max(60000).optional()
|
|
9
|
+
const pollIntervalMs = z.number().int().min(50).max(1000).optional()
|
|
10
|
+
const waitFor = z.object({
|
|
11
|
+
condition: z.enum(['exists', 'visible', 'clickable']).optional(),
|
|
12
|
+
timeoutMs: z.number().int().min(100).max(10000).optional(), pollIntervalMs,
|
|
13
|
+
match: z.object({ index: z.number().int().nonnegative().optional() }).strict().optional()
|
|
14
|
+
}).strict()
|
|
15
|
+
const target = { selector: selector.optional(), elementId: z.string().min(1).optional() }
|
|
16
|
+
const exactlyOneTarget = (value: { selector?: unknown, elementId?: string }) => Number(!!value.selector) + Number(!!value.elementId) === 1
|
|
17
|
+
const assertion = z.union([
|
|
18
|
+
z.object({ kind: z.literal('element_visible'), selector }).strict(),
|
|
19
|
+
z.object({ kind: z.literal('element_absent'), selector }).strict(),
|
|
20
|
+
z.object({ kind: z.literal('screen_fingerprint'), fingerprint: z.string().min(1) }).strict(),
|
|
21
|
+
z.object({ kind: z.literal('state_equals'), property: z.string().min(1), expected: z.union([z.boolean(), z.number().finite(), z.string()]), ...target }).strict().refine(exactlyOneTarget)
|
|
22
|
+
])
|
|
23
|
+
const id = z.string().min(1)
|
|
24
|
+
const step = z.union([
|
|
25
|
+
z.object({ id, type: z.literal('start_app'), appId: z.string().min(1), verificationMode: verificationMode.optional() }).strict(),
|
|
26
|
+
z.object({ id, type: z.literal('tap'), ...target, waitFor: waitFor.optional(), verificationMode: verificationMode.optional() }).strict().refine(exactlyOneTarget),
|
|
27
|
+
z.object({ id, type: z.literal('wait'), selector, condition: z.enum(['exists', 'not_exists', 'visible', 'clickable']).optional(), timeoutMs, pollIntervalMs }).strict(),
|
|
28
|
+
z.object({ id, type: z.literal('assert'), assertion, timeoutMs, pollIntervalMs }).strict()
|
|
29
|
+
])
|
|
30
|
+
export const tapElementControlsSchema = z.object({
|
|
31
|
+
elementId: z.string().min(1).optional(), selector: selector.optional(), waitFor: waitFor.optional()
|
|
32
|
+
}).strict().refine(value => Number(!!value.elementId) + Number(!!value.selector) === 1 && (!value.elementId || !value.waitFor))
|
|
33
|
+
|
|
34
|
+
export const journeySchema = z.object({
|
|
35
|
+
platform: z.enum(['android', 'ios']), deviceId: z.string().min(1).optional(),
|
|
36
|
+
responseMode: z.enum(['compact', 'debug']).default('compact'), captureOnFailure: z.boolean().default(false),
|
|
37
|
+
defaults: z.object({ verificationMode: verificationMode.optional(), actionTimeoutMs: z.number().int().finite().optional(), verificationTimeoutMs: z.number().int().finite().optional() }).strict().optional(),
|
|
38
|
+
steps: z.array(step).min(1).max(50)
|
|
39
|
+
}).strict().refine(value => new Set(value.steps.map(step => step.id)).size === value.steps.length, 'Step IDs must be unique')
|
|
40
|
+
|
|
41
|
+
const selectorJson = { type: 'object', additionalProperties: false, properties: {
|
|
42
|
+
text: { type: 'string', minLength: 1 }, resource_id: { type: 'string', minLength: 1 }, accessibility_id: { type: 'string', minLength: 1 }, contains: { type: 'boolean' }
|
|
43
|
+
}, anyOf: ['text', 'resource_id', 'accessibility_id'].map(key => ({ required: [key] })) }
|
|
44
|
+
const targetJson = { selector: selectorJson, elementId: { type: 'string', minLength: 1 } }
|
|
45
|
+
const oneTargetJson = [{ required: ['selector'], not: { required: ['elementId'] } }, { required: ['elementId'], not: { required: ['selector'] } }]
|
|
46
|
+
const intervalJson = { timeoutMs: { type: 'integer', minimum: 100, maximum: 60000 }, pollIntervalMs: { type: 'integer', minimum: 50, maximum: 1000 } }
|
|
47
|
+
const modeJson = { type: 'string', enum: ['none', 'light', 'full'] }
|
|
48
|
+
const objectJson = (properties: Record<string, unknown>, required: string[], extra = {}) => ({ type: 'object', additionalProperties: false, properties, required, ...extra })
|
|
49
|
+
export const journeyStepsJson = {
|
|
50
|
+
type: 'array', minItems: 1, maxItems: 50, items: { oneOf: [
|
|
51
|
+
objectJson({ id: { type: 'string', minLength: 1 }, type: { const: 'start_app' }, appId: { type: 'string', minLength: 1 }, verificationMode: modeJson }, ['id', 'type', 'appId']),
|
|
52
|
+
objectJson({ id: { type: 'string', minLength: 1 }, type: { const: 'tap' }, ...targetJson, verificationMode: modeJson,
|
|
53
|
+
waitFor: objectJson({ ...intervalJson, timeoutMs: { type: 'integer', minimum: 100, maximum: 10000 }, condition: { enum: ['exists', 'visible', 'clickable'] }, match: objectJson({ index: { type: 'integer', minimum: 0 } }, []) }, [])
|
|
54
|
+
}, ['id', 'type'], { oneOf: oneTargetJson }),
|
|
55
|
+
objectJson({ id: { type: 'string', minLength: 1 }, type: { const: 'wait' }, selector: selectorJson, condition: { enum: ['exists', 'not_exists', 'visible', 'clickable'] }, ...intervalJson }, ['id', 'type', 'selector']),
|
|
56
|
+
objectJson({ id: { type: 'string', minLength: 1 }, type: { const: 'assert' }, ...intervalJson, assertion: { oneOf: [
|
|
57
|
+
objectJson({ kind: { const: 'element_visible' }, selector: selectorJson }, ['kind', 'selector']),
|
|
58
|
+
objectJson({ kind: { const: 'element_absent' }, selector: selectorJson }, ['kind', 'selector']),
|
|
59
|
+
objectJson({ kind: { const: 'screen_fingerprint' }, fingerprint: { type: 'string', minLength: 1 } }, ['kind', 'fingerprint']),
|
|
60
|
+
objectJson({ kind: { const: 'state_equals' }, ...targetJson, property: { type: 'string', minLength: 1 }, expected: { type: ['boolean', 'number', 'string'] } }, ['kind', 'property', 'expected'], { oneOf: oneTargetJson })
|
|
61
|
+
] } }, ['id', 'type', 'assertion'])
|
|
62
|
+
] }
|
|
63
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { journeyStepsJson } from './journey-schema.js'
|
|
2
|
+
|
|
3
|
+
export const toolDefinitions: any[] = [
|
|
2
4
|
{
|
|
3
5
|
name: 'start_app',
|
|
4
6
|
description: `Purpose:
|
|
@@ -47,7 +49,11 @@ Failure Handling:
|
|
|
47
49
|
deviceId: {
|
|
48
50
|
type: 'string',
|
|
49
51
|
description: 'Device UDID (iOS) or Serial (Android). Defaults to booted/connected.'
|
|
50
|
-
}
|
|
52
|
+
},
|
|
53
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'], default: 'full' },
|
|
54
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' },
|
|
55
|
+
actionTimeoutMs: { type: 'number', minimum: 100, maximum: 30000 },
|
|
56
|
+
verificationTimeoutMs: { type: 'number', minimum: 100, maximum: 60000 }
|
|
51
57
|
},
|
|
52
58
|
required: ['platform', 'appId']
|
|
53
59
|
}
|
|
@@ -250,6 +256,7 @@ Failure Handling:
|
|
|
250
256
|
properties: {
|
|
251
257
|
reason: { type: 'string', description: 'Optional reason for snapshot' },
|
|
252
258
|
includeLogs: { type: 'boolean', description: 'Whether to include logs', default: true },
|
|
259
|
+
includeScreenshot: { type: 'boolean', description: 'Whether to collect screenshot bytes', default: true },
|
|
253
260
|
logLines: { type: 'number', description: 'Maximum number of log lines to include', default: 200 },
|
|
254
261
|
platform: { type: 'string', enum: ['android', 'ios'], description: 'Optional platform override' },
|
|
255
262
|
appId: { type: 'string', description: 'Optional appId to scope logs (package/bundle id)' },
|
|
@@ -307,7 +314,9 @@ Failure Handling:
|
|
|
307
314
|
deviceId: {
|
|
308
315
|
type: 'string',
|
|
309
316
|
description: 'Device Serial (Android) or UDID (iOS). Defaults to connected/booted device.'
|
|
310
|
-
}
|
|
317
|
+
},
|
|
318
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' },
|
|
319
|
+
sinceSnapshotRevision: { type: 'number', minimum: 1 }
|
|
311
320
|
},
|
|
312
321
|
required: ['platform']
|
|
313
322
|
}
|
|
@@ -729,7 +738,11 @@ Failure Handling:
|
|
|
729
738
|
deviceId: {
|
|
730
739
|
type: 'string',
|
|
731
740
|
description: 'Device Serial/UDID. Defaults to connected/booted device.'
|
|
732
|
-
}
|
|
741
|
+
},
|
|
742
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'], default: 'full' },
|
|
743
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' },
|
|
744
|
+
actionTimeoutMs: { type: 'number', minimum: 100, maximum: 30000 },
|
|
745
|
+
verificationTimeoutMs: { type: 'number', minimum: 100, maximum: 60000 }
|
|
733
746
|
},
|
|
734
747
|
required: ['x', 'y']
|
|
735
748
|
}
|
|
@@ -783,9 +796,14 @@ This tool reports execution success only. Verification must be done with a separ
|
|
|
783
796
|
elementId: {
|
|
784
797
|
type: 'string',
|
|
785
798
|
description: 'A unique element identifier returned by wait_for_ui'
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
799
|
+
},
|
|
800
|
+
selector: { type: 'object', properties: { text: { type: 'string' }, resource_id: { type: 'string' }, accessibility_id: { type: 'string' }, contains: { type: 'boolean' } } },
|
|
801
|
+
waitFor: { type: 'object', properties: { condition: { type: 'string', enum: ['exists', 'visible', 'clickable'] }, timeoutMs: { type: 'number', minimum: 100, maximum: 10000 }, pollIntervalMs: { type: 'number', minimum: 50, maximum: 1000 }, match: { type: 'object', properties: { index: { type: 'number' } } } } },
|
|
802
|
+
platform: { type: 'string', enum: ['android', 'ios'] },
|
|
803
|
+
deviceId: { type: 'string' },
|
|
804
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'], default: 'full' },
|
|
805
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' }
|
|
806
|
+
}
|
|
789
807
|
}
|
|
790
808
|
},
|
|
791
809
|
{
|
|
@@ -1062,6 +1080,20 @@ BEHAVIOUR after outcome:
|
|
|
1062
1080
|
required: ['uiChanged']
|
|
1063
1081
|
}
|
|
1064
1082
|
},
|
|
1083
|
+
{
|
|
1084
|
+
name: 'run_journey',
|
|
1085
|
+
description: 'Run an ordered, bounded sequence of start_app, tap, wait, and deterministic assertion steps against one device. Stops after the first failed step.',
|
|
1086
|
+
inputSchema: {
|
|
1087
|
+
type: 'object',
|
|
1088
|
+
properties: {
|
|
1089
|
+
platform: { type: 'string', enum: ['android', 'ios'] },
|
|
1090
|
+
deviceId: { type: 'string' },
|
|
1091
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'compact' },
|
|
1092
|
+
steps: { type: 'array', minItems: 1, maxItems: 50, items: { type: 'object', properties: { id: { type: 'string' }, type: { type: 'string', enum: ['start_app', 'tap', 'wait', 'assert'] } }, required: ['id', 'type'] } }
|
|
1093
|
+
},
|
|
1094
|
+
required: ['platform', 'steps']
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1065
1097
|
{
|
|
1066
1098
|
name: 'get_network_activity',
|
|
1067
1099
|
description: `Returns structured network events captured from platform logs since the last action.
|
|
@@ -1095,3 +1127,24 @@ Returns { requests: [], count: 0 } when no credible network signals are found.`,
|
|
|
1095
1127
|
}
|
|
1096
1128
|
}
|
|
1097
1129
|
]
|
|
1130
|
+
|
|
1131
|
+
// Keep the additive execution controls consistent across all action tools.
|
|
1132
|
+
for (const tool of toolDefinitions) {
|
|
1133
|
+
if (['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back'].includes(tool.name)) {
|
|
1134
|
+
Object.assign(tool.inputSchema.properties, {
|
|
1135
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'], default: 'full' },
|
|
1136
|
+
responseMode: { type: 'string', enum: ['compact', 'debug'], default: 'debug' },
|
|
1137
|
+
actionTimeoutMs: { type: 'integer', description: 'Dispatch budget in milliseconds, clamped to 100–30000.' },
|
|
1138
|
+
verificationTimeoutMs: { type: 'integer', description: 'Light observation budget in milliseconds, clamped to 100–60000.' }
|
|
1139
|
+
})
|
|
1140
|
+
}
|
|
1141
|
+
if (tool.name === 'tap_element') Object.assign(tool.inputSchema, { oneOf: [{ required: ['elementId'], not: { required: ['selector'] } }, { required: ['selector'], not: { required: ['elementId'] } }] })
|
|
1142
|
+
if (tool.name === 'run_journey') Object.assign(tool.inputSchema.properties, {
|
|
1143
|
+
steps: journeyStepsJson,
|
|
1144
|
+
captureOnFailure: { type: 'boolean', default: false },
|
|
1145
|
+
defaults: { type: 'object', additionalProperties: false, properties: {
|
|
1146
|
+
verificationMode: { type: 'string', enum: ['none', 'light', 'full'] },
|
|
1147
|
+
actionTimeoutMs: { type: 'integer' }, verificationTimeoutMs: { type: 'integer' }
|
|
1148
|
+
} }
|
|
1149
|
+
})
|
|
1150
|
+
}
|