mobile-debug-mcp 0.30.0 → 0.30.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 +876 -155
- package/dist/observe/index.js +1 -0
- package/dist/observe/snapshot-metadata.js +62 -1
- package/dist/server/tool-definitions.js +20 -11
- package/dist/server/tool-handlers.js +7 -5
- package/dist/server-core.js +1 -1
- package/docs/CHANGELOG.md +9 -0
- package/docs/ROADMAP.md +85 -209
- package/docs/{rfcs → specs-deltas}/013-wait-and-synchronization-reliability.md +15 -35
- package/docs/{rfcs → specs-deltas}/014-actionability-resolution.md +32 -30
- package/docs/specs-deltas/015-adjustable-control-support.md +541 -0
- package/package.json +1 -1
- package/skills/spec-review/SKILL.md +52 -0
- package/skills/spec-review/references/spec-review-checklist.md +13 -0
- package/skills/spec-review/references/spec-review-template.md +28 -0
- package/src/interact/index.ts +1008 -171
- package/src/observe/index.ts +1 -0
- package/src/observe/snapshot-metadata.ts +69 -2
- package/src/server/tool-definitions.ts +20 -11
- package/src/server/tool-handlers.ts +7 -5
- package/src/server-core.ts +1 -1
- package/src/types.ts +26 -0
- package/test/unit/interact/adjust_control.test.ts +690 -33
- package/test/unit/interact/subtree_collection.test.ts +24 -0
- package/test/unit/interact/tap_element.test.ts +71 -0
- package/test/unit/interact/wait_for_ui_change.test.ts +92 -1
- package/test/unit/observe/snapshot_metadata.test.ts +67 -0
- package/test/unit/server/contract.test.ts +4 -0
- package/skills/rfc-review/SKILL.md +0 -52
- package/skills/rfc-review/references/rfc-review-checklist.md +0 -12
- package/skills/rfc-review/references/rfc-review-template.md +0 -28
- /package/docs/{rfcs → specs-deltas}/001-state-verification.md +0 -0
- /package/docs/{rfcs → specs-deltas}/002-richer-element-identity +0 -0
- /package/docs/{rfcs → specs-deltas}/003-wait-and-synchronization-reliability.md +0 -0
- /package/docs/{rfcs → specs-deltas}/004-action-verification-routing.md +0 -0
- /package/docs/{rfcs → specs-deltas}/005-unified-action-execution-and-verification-model.md +0 -0
- /package/docs/{rfcs → specs-deltas}/006-runtime-action-instrumentation-and-binding-layer.md +0 -0
- /package/docs/{rfcs → specs-deltas}/007-actionability-resolution-and-executable-target-selection.md +0 -0
- /package/docs/{rfcs → specs-deltas}/008-adjustable-control-support-and-semantic-value-manipulation.md +0 -0
- /package/docs/{rfcs → specs-deltas}/009-semantic-control-modeling-for-custom-and-composite-controls.md +0 -0
- /package/docs/{rfcs → specs-deltas}/010-verification-stabilization-and-temporal-convergence.md +0 -0
- /package/docs/{rfcs → specs-deltas}/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +0 -0
- /package/docs/{rfcs → specs-deltas}/011.1-recovery-contract-types-and-runtime-wiring-spec.md +0 -0
- /package/docs/{rfcs → specs-deltas}/012-action-trace-and-xecution-observability.md +0 -0
package/src/observe/index.ts
CHANGED
|
@@ -358,6 +358,7 @@ export class ToolsObserve {
|
|
|
358
358
|
|
|
359
359
|
raw.snapshot_revision = raw.ui_tree?.snapshot_revision ?? snapshotMetadata.snapshot_revision
|
|
360
360
|
raw.captured_at_ms = raw.ui_tree?.captured_at_ms ?? snapshotMetadata.captured_at_ms
|
|
361
|
+
raw.snapshot_delta = raw.ui_tree?.snapshot_delta ?? snapshotMetadata.snapshot_delta ?? null
|
|
361
362
|
raw.loading_state = raw.ui_tree?.loading_state ?? snapshotMetadata.loading_state
|
|
362
363
|
|
|
363
364
|
const semantic = deriveSnapshotSemantic(raw)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import crypto from 'crypto'
|
|
2
|
-
import type { GetUITreeResponse, LoadingState, UIElement } from '../types.js'
|
|
2
|
+
import type { GetUITreeResponse, LoadingState, SnapshotDelta, UIElement } from '../types.js'
|
|
3
3
|
|
|
4
4
|
interface SnapshotState {
|
|
5
5
|
revision: number
|
|
6
6
|
signature: string | null
|
|
7
|
+
elementSignatures: Map<string, string>
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
const snapshotStateByDevice = new Map<string, SnapshotState>()
|
|
@@ -38,6 +39,63 @@ function stableElementSignature(element: UIElement) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function stableElementIdentity(element: UIElement, index: number) {
|
|
43
|
+
const stableId = normalize(element.stable_id)
|
|
44
|
+
if (stableId) return `stable:${stableId}`
|
|
45
|
+
|
|
46
|
+
return `fallback:${crypto.createHash('sha1').update(JSON.stringify({
|
|
47
|
+
text: normalize(element.text),
|
|
48
|
+
contentDescription: normalize(element.contentDescription),
|
|
49
|
+
resourceId: normalize(element.resourceId),
|
|
50
|
+
type: normalize(element.type),
|
|
51
|
+
bounds: normalizeBounds(element.bounds),
|
|
52
|
+
index
|
|
53
|
+
})).digest('hex')}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildElementSignatures(tree: Pick<GetUITreeResponse, 'elements'> | null | undefined) {
|
|
57
|
+
const signatures = new Map<string, string>()
|
|
58
|
+
const elements = Array.isArray(tree?.elements) ? tree!.elements! : []
|
|
59
|
+
|
|
60
|
+
for (let index = 0; index < elements.length; index++) {
|
|
61
|
+
const element = elements[index]
|
|
62
|
+
if (!element) continue
|
|
63
|
+
const identity = stableElementIdentity(element, index)
|
|
64
|
+
signatures.set(identity, crypto.createHash('sha1').update(JSON.stringify(stableElementSignature(element))).digest('hex'))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return signatures
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function summarizeSnapshotDelta(previous: SnapshotState | undefined, currentElements: Map<string, string>): SnapshotDelta | null {
|
|
71
|
+
if (!previous) return null
|
|
72
|
+
|
|
73
|
+
let added = 0
|
|
74
|
+
let removed = 0
|
|
75
|
+
let mutated = 0
|
|
76
|
+
|
|
77
|
+
for (const [identity, signature] of currentElements.entries()) {
|
|
78
|
+
const previousSignature = previous.elementSignatures.get(identity)
|
|
79
|
+
if (previousSignature === undefined) {
|
|
80
|
+
added++
|
|
81
|
+
} else if (previousSignature !== signature) {
|
|
82
|
+
mutated++
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const identity of previous.elementSignatures.keys()) {
|
|
87
|
+
if (!currentElements.has(identity)) removed++
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
previous_snapshot_revision: previous.revision,
|
|
92
|
+
added_elements: added,
|
|
93
|
+
removed_elements: removed,
|
|
94
|
+
mutated_elements: mutated,
|
|
95
|
+
total_elements: currentElements.size
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
41
99
|
export function computeSnapshotSignature(tree: Pick<GetUITreeResponse, 'elements' | 'screen' | 'resolution' | 'error'> | null | undefined): string | null {
|
|
42
100
|
if (!tree || tree.error) return null
|
|
43
101
|
|
|
@@ -83,6 +141,10 @@ export function deriveSnapshotMetadata(
|
|
|
83
141
|
) {
|
|
84
142
|
const signature = signatureOverride ?? computeSnapshotSignature(tree)
|
|
85
143
|
const previous = snapshotStateByDevice.get(deviceKey)
|
|
144
|
+
const hasValidTree = !!tree && !tree.error
|
|
145
|
+
const currentElementSignatures = hasValidTree
|
|
146
|
+
? buildElementSignatures(tree)
|
|
147
|
+
: previous?.elementSignatures ?? new Map<string, string>()
|
|
86
148
|
|
|
87
149
|
let revision = 1
|
|
88
150
|
if (previous) {
|
|
@@ -93,11 +155,16 @@ export function deriveSnapshotMetadata(
|
|
|
93
155
|
}
|
|
94
156
|
}
|
|
95
157
|
|
|
96
|
-
snapshotStateByDevice.set(deviceKey, {
|
|
158
|
+
snapshotStateByDevice.set(deviceKey, {
|
|
159
|
+
revision,
|
|
160
|
+
signature,
|
|
161
|
+
elementSignatures: currentElementSignatures
|
|
162
|
+
})
|
|
97
163
|
|
|
98
164
|
return {
|
|
99
165
|
snapshot_revision: revision,
|
|
100
166
|
captured_at_ms: Date.now(),
|
|
167
|
+
snapshot_delta: hasValidTree ? summarizeSnapshotDelta(previous, currentElementSignatures) : null,
|
|
101
168
|
loading_state: detectLoadingState(tree, source)
|
|
102
169
|
}
|
|
103
170
|
}
|
|
@@ -244,7 +244,7 @@ Failure Handling:
|
|
|
244
244
|
},
|
|
245
245
|
{
|
|
246
246
|
name: 'capture_debug_snapshot',
|
|
247
|
-
description: 'Capture a complete debug snapshot (raw observation layer plus optional derived semantic layer). Returns structured JSON with snapshot_revision, captured_at_ms, and loading_state when detectable.',
|
|
247
|
+
description: 'Capture a complete debug snapshot (raw observation layer plus optional derived semantic layer). Returns structured JSON with snapshot_revision, captured_at_ms, snapshot_delta, and loading_state when detectable.',
|
|
248
248
|
inputSchema: {
|
|
249
249
|
type: 'object',
|
|
250
250
|
properties: {
|
|
@@ -295,7 +295,7 @@ Failure Handling:
|
|
|
295
295
|
},
|
|
296
296
|
{
|
|
297
297
|
name: 'get_ui_tree',
|
|
298
|
-
description: 'Get the current UI hierarchy from an Android device or iOS simulator. Returns a structured JSON representation of the screen content with snapshot metadata when available.',
|
|
298
|
+
description: 'Get the current UI hierarchy from an Android device or iOS simulator. Returns a structured JSON representation of the screen content with snapshot metadata and incremental delta signals when available.',
|
|
299
299
|
inputSchema: {
|
|
300
300
|
type: 'object',
|
|
301
301
|
properties: {
|
|
@@ -376,11 +376,14 @@ Inputs:
|
|
|
376
376
|
- expected_change (optional): hierarchy_diff, text_change, or state_change
|
|
377
377
|
- timeout_ms (optional)
|
|
378
378
|
- stability_window_ms (optional)
|
|
379
|
+
- scope (optional): screen or subtree
|
|
380
|
+
- target (optional): element_id when scope=subtree
|
|
379
381
|
|
|
380
382
|
Guidance:
|
|
381
383
|
- Prefer wait_for_screen_change for navigation transitions.
|
|
382
384
|
- Prefer wait_for_ui_change for in-place mutations and non-navigation updates.
|
|
383
385
|
- Use the returned snapshot_revision as the observed synchronization point when available.
|
|
386
|
+
- Scoped waits return scope-aware stability metadata and a lightweight change summary.
|
|
384
387
|
|
|
385
388
|
Failure Handling:
|
|
386
389
|
- TIMEOUT means the UI did not change in a stable way within the allotted time.`,
|
|
@@ -390,6 +393,8 @@ Failure Handling:
|
|
|
390
393
|
platform: { type: 'string', enum: ['android', 'ios'], description: 'Optional platform override (android|ios)' },
|
|
391
394
|
deviceId: { type: 'string', description: 'Optional device id/udid to target' },
|
|
392
395
|
expected_change: { type: 'string', enum: ['hierarchy_diff', 'text_change', 'state_change'], description: 'Optional type of UI change to wait for' },
|
|
396
|
+
scope: { type: 'string', enum: ['screen', 'subtree'], default: 'screen', description: 'Synchronization scope for the wait' },
|
|
397
|
+
target: { type: 'string', description: 'Target element_id when scope is subtree' },
|
|
393
398
|
timeout_ms: { type: 'number', description: 'Timeout in ms to wait for change (default 60000)', default: 60000 },
|
|
394
399
|
stability_window_ms: { type: 'number', description: 'How long the change must remain stable before success (default 300)', default: 300 }
|
|
395
400
|
}
|
|
@@ -563,11 +568,11 @@ Adjust a numeric control value with verification.
|
|
|
563
568
|
This is the initial adjustable-control surface for slider-like controls and other controls that expose a numeric value or value_range.
|
|
564
569
|
|
|
565
570
|
Inputs:
|
|
566
|
-
- selector or element_id
|
|
571
|
+
- exactly one of selector or element_id
|
|
567
572
|
- property (defaults to "value")
|
|
568
573
|
- targetValue
|
|
569
|
-
- tolerance (optional)
|
|
570
|
-
- maxAttempts (optional)
|
|
574
|
+
- tolerance (optional; discrete default is 0, continuous default is max(step / 2, 1% of range) when omitted)
|
|
575
|
+
- maxAttempts (optional; defaults to 5)
|
|
571
576
|
- platform/deviceId (optional)
|
|
572
577
|
|
|
573
578
|
Output Structure:
|
|
@@ -579,14 +584,14 @@ Output Structure:
|
|
|
579
584
|
- success = true when the control converges within tolerance
|
|
580
585
|
|
|
581
586
|
Verification Guidance:
|
|
582
|
-
- Prefer direct
|
|
587
|
+
- Prefer direct platform adjustment when the runtime exposes it; then verified increment/decrement actions; use coordinate fallback only as degraded mode
|
|
583
588
|
- Use expect_state for the control value readback
|
|
584
|
-
-
|
|
589
|
+
- Do not use derived semantic metadata as proof of success
|
|
585
590
|
|
|
586
591
|
Failure Handling:
|
|
587
592
|
- ELEMENT_NOT_FOUND → re-resolve the control
|
|
588
593
|
- ELEMENT_NOT_INTERACTABLE → the control cannot be adjusted through the current runtime
|
|
589
|
-
-
|
|
594
|
+
- CONTROL_CONVERGENCE_FAILED → the control did not converge within bounded retries or the requested value is out of range
|
|
590
595
|
- UNKNOWN → capture a snapshot and stop`,
|
|
591
596
|
inputSchema: {
|
|
592
597
|
type: 'object',
|
|
@@ -603,12 +608,16 @@ Failure Handling:
|
|
|
603
608
|
element_id: { type: 'string', description: 'Optional previously resolved element identifier.' },
|
|
604
609
|
property: { type: 'string', description: 'Readable numeric state property to adjust.', default: 'value' },
|
|
605
610
|
targetValue: { type: 'number', description: 'Target numeric value.' },
|
|
606
|
-
tolerance: { type: 'number', description: 'Accepted numeric tolerance around the target value.
|
|
607
|
-
maxAttempts: { type: 'number', description: 'Maximum adjustment attempts.', default:
|
|
611
|
+
tolerance: { type: 'number', description: 'Accepted numeric tolerance around the target value. Defaults are derived from the readable control domain.' },
|
|
612
|
+
maxAttempts: { type: 'number', description: 'Maximum adjustment attempts.', default: 5 },
|
|
608
613
|
platform: { type: 'string', enum: ['android', 'ios'], description: 'Optional platform override' },
|
|
609
614
|
deviceId: { type: 'string', description: 'Optional device serial/udid' }
|
|
610
615
|
},
|
|
611
|
-
required: ['targetValue']
|
|
616
|
+
required: ['targetValue'],
|
|
617
|
+
oneOf: [
|
|
618
|
+
{ required: ['selector'] },
|
|
619
|
+
{ required: ['element_id'] }
|
|
620
|
+
]
|
|
612
621
|
}
|
|
613
622
|
},
|
|
614
623
|
{
|
|
@@ -282,12 +282,12 @@ async function handleAdjustControl(args: ToolCallArgs) {
|
|
|
282
282
|
const element_id = getStringArg(args, 'element_id')
|
|
283
283
|
const property = getStringArg(args, 'property') ?? 'value'
|
|
284
284
|
const targetValue = requireNumberArg(args, 'targetValue')
|
|
285
|
-
const tolerance = getNumberArg(args, 'tolerance')
|
|
286
|
-
const maxAttempts = getNumberArg(args, 'maxAttempts')
|
|
285
|
+
const tolerance = getNumberArg(args, 'tolerance')
|
|
286
|
+
const maxAttempts = getNumberArg(args, 'maxAttempts')
|
|
287
287
|
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
288
288
|
const deviceId = getStringArg(args, 'deviceId')
|
|
289
|
-
if (!selector && !element_id) {
|
|
290
|
-
throw new Error('
|
|
289
|
+
if ((!selector && !element_id) || (selector && element_id)) {
|
|
290
|
+
throw new Error('Exactly one of selector or element_id argument is required')
|
|
291
291
|
}
|
|
292
292
|
const res = await ToolsInteract.adjustControlHandler({
|
|
293
293
|
selector: selector ?? undefined,
|
|
@@ -321,7 +321,9 @@ async function handleWaitForUIChange(args: ToolCallArgs) {
|
|
|
321
321
|
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
|
|
322
322
|
const stability_window_ms = getNumberArg(args, 'stability_window_ms') ?? 300
|
|
323
323
|
const expected_change = getStringArg(args, 'expected_change') as 'hierarchy_diff' | 'text_change' | 'state_change' | undefined
|
|
324
|
-
const
|
|
324
|
+
const scope = getStringArg(args, 'scope') as 'screen' | 'subtree' | undefined
|
|
325
|
+
const target = getStringArg(args, 'target')
|
|
326
|
+
const res = await ToolsInteract.waitForUIChangeHandler({ platform, deviceId, timeout_ms, stability_window_ms, expected_change, scope, target })
|
|
325
327
|
return wrapResponse(res)
|
|
326
328
|
}
|
|
327
329
|
|
package/src/server-core.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -91,7 +91,9 @@ export interface UIElementState {
|
|
|
91
91
|
value_range?: {
|
|
92
92
|
min: number;
|
|
93
93
|
max: number;
|
|
94
|
+
step?: number | null;
|
|
94
95
|
} | null;
|
|
96
|
+
step?: number | null;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
export interface SelectorConfidence {
|
|
@@ -152,6 +154,14 @@ export interface LoadingState {
|
|
|
152
154
|
source: string;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
export interface SnapshotDelta {
|
|
158
|
+
previous_snapshot_revision: number | null;
|
|
159
|
+
added_elements: number;
|
|
160
|
+
removed_elements: number;
|
|
161
|
+
mutated_elements: number;
|
|
162
|
+
total_elements: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
155
165
|
export interface CaptureAndroidScreenResponse {
|
|
156
166
|
device: DeviceInfo;
|
|
157
167
|
screenshot: string; // base64 encoded string
|
|
@@ -207,6 +217,7 @@ export interface GetUITreeResponse {
|
|
|
207
217
|
elements: UIElement[];
|
|
208
218
|
snapshot_revision: number;
|
|
209
219
|
captured_at_ms: number;
|
|
220
|
+
snapshot_delta?: SnapshotDelta | null;
|
|
210
221
|
loading_state?: LoadingState | null;
|
|
211
222
|
error?: string;
|
|
212
223
|
}
|
|
@@ -231,6 +242,7 @@ export interface CaptureDebugSnapshotRawResponse {
|
|
|
231
242
|
timestamp: number;
|
|
232
243
|
snapshot_revision: number;
|
|
233
244
|
captured_at_ms: number;
|
|
245
|
+
snapshot_delta?: SnapshotDelta | null;
|
|
234
246
|
reason: string;
|
|
235
247
|
activity: string | null;
|
|
236
248
|
fingerprint: string | null;
|
|
@@ -497,11 +509,25 @@ export interface WaitForUIChangeResponse {
|
|
|
497
509
|
success: boolean;
|
|
498
510
|
observed_change: 'hierarchy_diff' | 'text_change' | 'state_change' | null;
|
|
499
511
|
snapshot_revision?: number;
|
|
512
|
+
snapshot_freshness_ms?: number | null;
|
|
513
|
+
scope?: 'screen' | 'subtree';
|
|
514
|
+
target?: string | null;
|
|
515
|
+
stability_state?: 'transient' | 'stable';
|
|
516
|
+
change_summary?: {
|
|
517
|
+
total_elements: number;
|
|
518
|
+
added_elements: number;
|
|
519
|
+
removed_elements: number;
|
|
520
|
+
mutated_elements: number;
|
|
521
|
+
} | null;
|
|
500
522
|
timeout: boolean;
|
|
501
523
|
elapsed_ms: number;
|
|
502
524
|
expected_change?: 'hierarchy_diff' | 'text_change' | 'state_change';
|
|
503
525
|
reason?: string;
|
|
504
526
|
loading_state?: LoadingState | null;
|
|
527
|
+
error?: {
|
|
528
|
+
code: 'INVALID_SCOPE' | 'ELEMENT_NOT_FOUND';
|
|
529
|
+
message: string;
|
|
530
|
+
};
|
|
505
531
|
}
|
|
506
532
|
|
|
507
533
|
export interface SwipeResponse {
|