mobile-debug-mcp 0.25.0 → 0.25.1

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.
@@ -105,7 +105,13 @@ export class ToolsInteract {
105
105
  class: el.type ?? el.class ?? null,
106
106
  bounds,
107
107
  index,
108
- elementId
108
+ elementId,
109
+ state: el.state ?? null,
110
+ stable_id: el.stable_id ?? null,
111
+ role: el.role ?? null,
112
+ test_tag: el.test_tag ?? null,
113
+ selector: el.selector ?? null,
114
+ semantic: el.semantic ?? null
109
115
  };
110
116
  }
111
117
  static _rememberResolvedElement(elementId, context) {
@@ -138,7 +144,12 @@ export class ToolsInteract {
138
144
  class: element.type ?? element.class ?? null,
139
145
  bounds: ToolsInteract._normalizeBounds(element.bounds),
140
146
  index,
141
- state: element.state ?? null
147
+ state: element.state ?? null,
148
+ stable_id: element.stable_id ?? null,
149
+ role: element.role ?? null,
150
+ test_tag: element.test_tag ?? null,
151
+ selector: element.selector ?? null,
152
+ semantic: element.semantic ?? null
142
153
  };
143
154
  }
144
155
  static _actionFailure(actionId, timestamp, actionType, selector, resolved, failureCode, retryable, uiFingerprintBefore, uiFingerprintAfter) {
@@ -542,6 +553,11 @@ export class ToolsInteract {
542
553
  bounds: boundsObj,
543
554
  clickable: !!best.clickable,
544
555
  enabled: !!best.enabled,
556
+ stable_id: best.stable_id ?? null,
557
+ role: best.role ?? null,
558
+ test_tag: best.test_tag ?? null,
559
+ selector: best.selector ?? null,
560
+ semantic: best.semantic ?? null,
545
561
  tapCoordinates,
546
562
  telemetry: {
547
563
  matchedIndex: best?._index ?? null,
@@ -940,7 +956,12 @@ export class ToolsInteract {
940
956
  class: result.element.class ?? null,
941
957
  bounds: result.element.bounds ?? null,
942
958
  index: typeof result.element.index === 'number' ? result.element.index : null,
943
- state: result.element.state ?? null
959
+ state: result.element.state ?? null,
960
+ stable_id: result.element.stable_id ?? null,
961
+ role: result.element.role ?? null,
962
+ test_tag: result.element.test_tag ?? null,
963
+ selector: result.element.selector ?? null,
964
+ semantic: result.element.semantic ?? null
944
965
  },
945
966
  observed: {
946
967
  status: result.status,
@@ -955,7 +976,12 @@ export class ToolsInteract {
955
976
  class: result.element.class ?? null,
956
977
  bounds: result.element.bounds ?? null,
957
978
  index: typeof result.element.index === 'number' ? result.element.index : null,
958
- state: result.element.state ?? null
979
+ state: result.element.state ?? null,
980
+ stable_id: result.element.stable_id ?? null,
981
+ role: result.element.role ?? null,
982
+ test_tag: result.element.test_tag ?? null,
983
+ selector: result.element.selector ?? null,
984
+ semantic: result.element.semantic ?? null
959
985
  }
960
986
  },
961
987
  reason: 'selector is visible'
@@ -45,6 +45,65 @@ function parseIOSNumber(value) {
45
45
  const parsed = Number(value);
46
46
  return Number.isFinite(parsed) ? parsed : null;
47
47
  }
48
+ function normalizeIOSType(value) {
49
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
50
+ }
51
+ function inferIOSRole(type, traits) {
52
+ if (/slider|adjustable/.test(type) || traits.some((trait) => /adjustable|slider/.test(trait)))
53
+ return 'slider';
54
+ if (/button/.test(type) || traits.some((trait) => /button/.test(trait)))
55
+ return 'button';
56
+ if (/cell/.test(type))
57
+ return 'cell';
58
+ if (/switch/.test(type))
59
+ return 'switch';
60
+ if (/text field|textfield|search field/.test(type))
61
+ return 'text_field';
62
+ if (/image/.test(type))
63
+ return 'image';
64
+ if (/window|application|group|scroll view|collection view/.test(type))
65
+ return 'container';
66
+ return null;
67
+ }
68
+ function getIOSStableId(node) {
69
+ const candidates = [node.AXIdentifier, node.accessibilityIdentifier, node.identifier, node.AXUniqueId];
70
+ for (const candidate of candidates) {
71
+ if (typeof candidate === 'string' && candidate.trim().length > 0)
72
+ return candidate;
73
+ }
74
+ return null;
75
+ }
76
+ function buildIOSSelectorConfidence(source) {
77
+ switch (source) {
78
+ case 'identifier':
79
+ return { score: 1, reason: 'accessibility_identifier' };
80
+ case 'label':
81
+ return { score: 0.9, reason: 'label_match' };
82
+ case 'value':
83
+ return { score: 0.75, reason: 'value_match' };
84
+ case 'type':
85
+ return { score: 0.35, reason: 'type_match' };
86
+ default:
87
+ return null;
88
+ }
89
+ }
90
+ function buildIOSSelector(type, label, value, stableId) {
91
+ if (stableId)
92
+ return { value: stableId, confidence: buildIOSSelectorConfidence('identifier') };
93
+ if (label)
94
+ return { value: label, confidence: buildIOSSelectorConfidence('label') };
95
+ if (value)
96
+ return { value: value, confidence: buildIOSSelectorConfidence('value') };
97
+ if (type)
98
+ return { value: type, confidence: buildIOSSelectorConfidence('type') };
99
+ return null;
100
+ }
101
+ function buildIOSSemantic(type, traits) {
102
+ return {
103
+ is_clickable: traits.includes("UIAccessibilityTraitButton") || /adjustable|slider/.test(type) || type === "Button" || type === "Cell",
104
+ is_container: /window|application|group|scroll view|collection view/.test(type)
105
+ };
106
+ }
48
107
  function isIOSAdjustable(node, type, traits) {
49
108
  return /slider|adjustable|stepper|progress/i.test(type) || traits.some((trait) => /adjustable|slider|progress/i.test(trait));
50
109
  }
@@ -99,6 +158,11 @@ export function traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
99
158
  const frame = node.AXFrame || node.frame;
100
159
  const traits = node.AXTraits || [];
101
160
  const state = extractIOSState(node, type, label, value, traits);
161
+ const normalizedType = normalizeIOSType(type);
162
+ const stableId = getIOSStableId(node);
163
+ const selector = buildIOSSelector(type, label, value, stableId);
164
+ const semantic = buildIOSSemantic(normalizedType, traits);
165
+ const role = inferIOSRole(normalizedType, traits);
102
166
  const clickable = traits.includes("UIAccessibilityTraitButton") || type === "Button" || type === "Cell";
103
167
  const isUseful = clickable || (label && label.length > 0) || (value && value.length > 0) || type === "Application" || type === "Window";
104
168
  if (isUseful) {
@@ -107,14 +171,19 @@ export function traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
107
171
  text: label,
108
172
  contentDescription: value,
109
173
  type: type,
110
- resourceId: node.AXUniqueId || null,
174
+ resourceId: stableId,
111
175
  clickable: clickable,
112
176
  enabled: true,
113
177
  visible: true,
114
178
  bounds: bounds,
115
179
  center: getCenter(bounds),
116
180
  depth: depth,
117
- state
181
+ state,
182
+ stable_id: stableId,
183
+ role,
184
+ test_tag: stableId,
185
+ selector,
186
+ semantic
118
187
  };
119
188
  if (parentIndex !== -1) {
120
189
  element.parentId = parentIndex;
@@ -6,7 +6,7 @@ import { handleToolCall } from './server/tool-handlers.js';
6
6
  export { wrapResponse, toolDefinitions, handleToolCall };
7
7
  export const serverInfo = {
8
8
  name: 'mobile-debug-mcp',
9
- version: '0.25.0'
9
+ version: '0.25.1'
10
10
  };
11
11
  export function createServer() {
12
12
  const server = new Server(serverInfo, {
@@ -356,6 +356,59 @@ function parseNumberAttr(value) {
356
356
  const parsed = Number(value);
357
357
  return Number.isFinite(parsed) ? parsed : null;
358
358
  }
359
+ function normalizeClassName(value) {
360
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
361
+ }
362
+ function inferAndroidRole(className) {
363
+ if (/seekbar|slider|progress/.test(className))
364
+ return 'slider';
365
+ if (/switch|toggle/.test(className))
366
+ return 'switch';
367
+ if (/checkbox/.test(className))
368
+ return 'checkbox';
369
+ if (/radiobutton|radio/.test(className))
370
+ return 'radio';
371
+ if (/edittext|textfield|search/.test(className))
372
+ return 'text_field';
373
+ if (/button|fab/.test(className))
374
+ return 'button';
375
+ if (/imageview|icon/.test(className))
376
+ return 'image';
377
+ if (/recyclerview|scroll|layout|viewgroup|frame/.test(className))
378
+ return 'container';
379
+ return null;
380
+ }
381
+ function buildAndroidSelectorConfidence(source) {
382
+ switch (source) {
383
+ case 'resource_id':
384
+ return { score: 1, reason: 'resource_id' };
385
+ case 'content_desc':
386
+ return { score: 0.9, reason: 'content_description' };
387
+ case 'text':
388
+ return { score: 0.6, reason: 'text_match' };
389
+ case 'class':
390
+ return { score: 0.35, reason: 'class_match' };
391
+ default:
392
+ return null;
393
+ }
394
+ }
395
+ function buildAndroidSelector(text, contentDescription, resourceId, className) {
396
+ if (resourceId)
397
+ return { value: resourceId, confidence: buildAndroidSelectorConfidence('resource_id') };
398
+ if (contentDescription)
399
+ return { value: contentDescription, confidence: buildAndroidSelectorConfidence('content_desc') };
400
+ if (text)
401
+ return { value: text, confidence: buildAndroidSelectorConfidence('text') };
402
+ if (className)
403
+ return { value: className, confidence: buildAndroidSelectorConfidence('class') };
404
+ return null;
405
+ }
406
+ function buildAndroidSemantic(clickable, className) {
407
+ return {
408
+ is_clickable: clickable,
409
+ is_container: /recyclerview|scroll|layout|viewgroup|frame/.test(className)
410
+ };
411
+ }
359
412
  function isSliderLikeAndroid(node) {
360
413
  const className = String(node['@_class'] || '').toLowerCase();
361
414
  return /seekbar|slider|range|progress/i.test(className);
@@ -426,22 +479,34 @@ export function traverseNode(node, elements, parentIndex = -1, depth = 0) {
426
479
  const text = node['@_text'] || null;
427
480
  const contentDescription = node['@_content-desc'] || null;
428
481
  const clickable = node['@_clickable'] === 'true';
482
+ const className = String(node['@_class'] || 'unknown');
429
483
  const bounds = parseBounds(node['@_bounds'] || '[0,0][0,0]');
430
484
  const state = extractAndroidState(node);
485
+ const role = inferAndroidRole(normalizeClassName(className));
486
+ const resourceId = typeof node['@_resource-id'] === 'string' && node['@_resource-id'].trim().length > 0 ? node['@_resource-id'] : null;
487
+ const stableId = resourceId ?? (typeof contentDescription === 'string' && contentDescription.trim().length > 0 ? contentDescription : null);
488
+ const testTag = stableId;
489
+ const selector = buildAndroidSelector(text, contentDescription, resourceId, normalizeClassName(className));
490
+ const semantic = buildAndroidSemantic(clickable, normalizeClassName(className));
431
491
  const isUseful = clickable || (text && text.length > 0) || (contentDescription && contentDescription.length > 0);
432
492
  if (isUseful) {
433
493
  const element = {
434
494
  text,
435
495
  contentDescription,
436
- type: node['@_class'] || 'unknown',
437
- resourceId: node['@_resource-id'] || null,
496
+ type: className,
497
+ resourceId,
438
498
  clickable,
439
499
  enabled: node['@_enabled'] === 'true',
440
500
  visible: true,
441
501
  bounds,
442
502
  center: getCenter(bounds),
443
503
  depth,
444
- state
504
+ state,
505
+ stable_id: stableId,
506
+ role,
507
+ test_tag: testTag,
508
+ selector,
509
+ semantic
445
510
  };
446
511
  if (parentIndex !== -1) {
447
512
  element.parentId = parentIndex;
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## [0.25.1]
6
+ - Platform-native element identity metadata for UI targeting
7
+ - Hierarchy-independent element references
8
+ - Selector confidence metadata for reliability
9
+ - Structured fallback resolution strategy
10
+
5
11
  ## [0.25.0]
6
12
  - Introduces the `expect_state` tool and a standardized state object for UI elements across Android and iOS.
7
13
 
package/docs/ROADMAP.md CHANGED
@@ -26,11 +26,27 @@ Higher task success with fewer retries.
26
26
 
27
27
  ---
28
28
 
29
+ # Completed
30
+
31
+ These priorities are done and kept here for history:
32
+
33
+ - Priority 1 — Stronger State Verification
34
+ - Priority 2 — Richer Element Identity
35
+
36
+ Completion notes:
37
+
38
+ - State-aware verification is now implemented and wired through the tool surface.
39
+ - Platform-native element metadata and selector-confidence hints are now part of the runtime contract.
40
+
41
+ ---
42
+
29
43
  # Priority 1 — Stronger State Verification
30
44
 
31
45
  ## Why first
32
46
  Highest leverage improvement.
33
47
 
48
+ **Status:** Completed
49
+
34
50
  Most failures are not “can’t act,” they’re:
35
51
  - uncertain state
36
52
  - weak verification
@@ -68,6 +84,8 @@ Blocks or strengthens:
68
84
  ## Why second
69
85
  Directly reduces selector brittleness.
70
86
 
87
+ **Status:** Completed
88
+
71
89
  Improves:
72
90
  - targeting stability
73
91
  - repeatability
@@ -385,4 +403,4 @@ Still out of scope:
385
403
  - Recovery planning logic
386
404
  - Autonomous retry strategy
387
405
  - MCP-level agent orchestration
388
- - Autonomous recovery hinting (future consideration only)
406
+ - Autonomous recovery hinting (future consideration only)