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.
Files changed (44) hide show
  1. package/dist/interact/index.js +876 -155
  2. package/dist/observe/index.js +1 -0
  3. package/dist/observe/snapshot-metadata.js +62 -1
  4. package/dist/server/tool-definitions.js +20 -11
  5. package/dist/server/tool-handlers.js +7 -5
  6. package/dist/server-core.js +1 -1
  7. package/docs/CHANGELOG.md +9 -0
  8. package/docs/ROADMAP.md +85 -209
  9. package/docs/{rfcs → specs-deltas}/013-wait-and-synchronization-reliability.md +15 -35
  10. package/docs/{rfcs → specs-deltas}/014-actionability-resolution.md +32 -30
  11. package/docs/specs-deltas/015-adjustable-control-support.md +541 -0
  12. package/package.json +1 -1
  13. package/skills/spec-review/SKILL.md +52 -0
  14. package/skills/spec-review/references/spec-review-checklist.md +13 -0
  15. package/skills/spec-review/references/spec-review-template.md +28 -0
  16. package/src/interact/index.ts +1008 -171
  17. package/src/observe/index.ts +1 -0
  18. package/src/observe/snapshot-metadata.ts +69 -2
  19. package/src/server/tool-definitions.ts +20 -11
  20. package/src/server/tool-handlers.ts +7 -5
  21. package/src/server-core.ts +1 -1
  22. package/src/types.ts +26 -0
  23. package/test/unit/interact/adjust_control.test.ts +690 -33
  24. package/test/unit/interact/subtree_collection.test.ts +24 -0
  25. package/test/unit/interact/tap_element.test.ts +71 -0
  26. package/test/unit/interact/wait_for_ui_change.test.ts +92 -1
  27. package/test/unit/observe/snapshot_metadata.test.ts +67 -0
  28. package/test/unit/server/contract.test.ts +4 -0
  29. package/skills/rfc-review/SKILL.md +0 -52
  30. package/skills/rfc-review/references/rfc-review-checklist.md +0 -12
  31. package/skills/rfc-review/references/rfc-review-template.md +0 -28
  32. /package/docs/{rfcs → specs-deltas}/001-state-verification.md +0 -0
  33. /package/docs/{rfcs → specs-deltas}/002-richer-element-identity +0 -0
  34. /package/docs/{rfcs → specs-deltas}/003-wait-and-synchronization-reliability.md +0 -0
  35. /package/docs/{rfcs → specs-deltas}/004-action-verification-routing.md +0 -0
  36. /package/docs/{rfcs → specs-deltas}/005-unified-action-execution-and-verification-model.md +0 -0
  37. /package/docs/{rfcs → specs-deltas}/006-runtime-action-instrumentation-and-binding-layer.md +0 -0
  38. /package/docs/{rfcs → specs-deltas}/007-actionability-resolution-and-executable-target-selection.md +0 -0
  39. /package/docs/{rfcs → specs-deltas}/008-adjustable-control-support-and-semantic-value-manipulation.md +0 -0
  40. /package/docs/{rfcs → specs-deltas}/009-semantic-control-modeling-for-custom-and-composite-controls.md +0 -0
  41. /package/docs/{rfcs → specs-deltas}/010-verification-stabilization-and-temporal-convergence.md +0 -0
  42. /package/docs/{rfcs → specs-deltas}/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +0 -0
  43. /package/docs/{rfcs → specs-deltas}/011.1-recovery-contract-types-and-runtime-wiring-spec.md +0 -0
  44. /package/docs/{rfcs → specs-deltas}/012-action-trace-and-xecution-observability.md +0 -0
@@ -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, { revision, signature })
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 target placement when value_range is available; fall back to a drag only if the direct tap does not converge
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
- - Treat coordinate fallback as degraded mode
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
- - TIMEOUT → the control did not converge within bounded retries
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.', default: 0 },
607
- maxAttempts: { type: 'number', description: 'Maximum adjustment attempts.', default: 3 },
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') ?? 0
286
- const maxAttempts = getNumberArg(args, 'maxAttempts') ?? 3
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('Missing selector or element_id argument')
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 res = await ToolsInteract.waitForUIChangeHandler({ platform, deviceId, timeout_ms, stability_window_ms, expected_change })
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
 
@@ -16,7 +16,7 @@ export { wrapResponse, toolDefinitions, handleToolCall }
16
16
 
17
17
  export const serverInfo = {
18
18
  name: 'mobile-debug-mcp',
19
- version: '0.30.0'
19
+ version: '0.30.2'
20
20
  }
21
21
 
22
22
  export function createServer() {
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 {